Service Workers at Scale, Part II: Handling Fallback Resources

Programming - Apr 19, 2024

Part I of this series established that some of the challenges in building service workers for complex websites stem from having to accommodate page request variations. In this iteration, we’ll touch on how to handle similar variations in the acquiring and delivering of fallback resources.

A familiar pattern for caching dependencies is to request them in bulk during the installation stage. We began with this approach in our own service worker, specifying path strings for all fallback resources:


addEventListener('install', event => {
  event.waitUntil(
    caches.open('dependencies')
      .then(cache => {
        return cache.addAll([
          '/offline',
          '/books-offline',
          '/assets/offline-avatar.png',
          '/assets/offline-photo.png'
        ]);
      )
      .catch(err => console.warn(err))
      .then(skipWaiting())
  );
});
Code language: JavaScript (javascript)

Cache.addAll() converts each element of the array it receives into a new Request object, so you can conveniently pass it an array of strings. This simple approach worked well during local development, but there were some issues on our more complicated test server. We needed more control over the requests made by Cache.addAll(), so we created them explicitly:


cache.addAll([
  new Request('/offline', {...}),
  new Request('/books-offline', {...}),
  new Request('/assets/offline-avatar.png', {...}),
  new Request('/assets/offline-photo.png', {...})
]);
Code language: JavaScript (javascript)

Constructing our own requests gave us the ability to override their default options. The necessity to supply these options became obvious once we saw 401 responses to our fallback page requests. The server had enabled HTTP auth, so requests sent from our worker in attempt to pre-cache resources were failing.

The credentials option solved this problem, allowing our requests to break through the authentication barrier:


cache.addAll([
  new Request('/offline', {credentials: 'same-origin'}),
  // ...
]);
Code language: JavaScript (javascript)

We also decided to use the cache option for future-proofing. This will be useful for controlling how requests interact with the HTTP cache. While it currently only works in Firefox Developer Edition, we included it to make sure pre-cached responses are freshFootnote 1 :


cache.addAll([
  new Request('/assets/offline-photo.png', {cache: 'reload'}),
  // ...
]);
Code language: JavaScript (javascript)

For an overview of other cache option values, check out Jake Archibald’s article with various examples of service worker and HTTP cache interoperability.

Our pre-cache items include generic images intended to serve as “fallbacks” for unfulfilled requests. We needed to account for two different fallback image types, each with their own visual treatment:

  1. Images embedded in articles
  2. Avatars for authors and commenters

To determine which fallback should be used for a given request, we associated each with a URL hostname:


const fallbackImages = new Map([
  [location.hostname, '/assets/offline-photo.png'],
  ['secure.gravatar.com', '/assets/offline-avatar.png']
]);
Code language: JavaScript (javascript)

Using a map like this, we can conveniently lookup the proper fallback based on the URL of an unfulfilled request:


function matchFallback (req) {
  const {hostname} = new URL(req.url);

  if (isImageRequest(req)) {
    const image = fallbackImages.get(hostname);
    return caches.match(image);
  }

  // ...
}
Code language: JavaScript (javascript)

As with our fallback images, we also needed to accommodate a bit of variation in our handling of fallback pages. Some pages that we wanted to make available offline had too many images to justify pre-caching. In these cases, simplified versions of those pages (minus the images) were created to use as substitutes, as if they were redirected.

Because all of the pages with offline variations are local, they can be mapped by their URL pathname, and incorporated into our matchFallback() handler accordingly:


const fallbackPages = new Map([
  ['/books', '/books-offline'],
  ['/workshops', '/workshops-offline']
]);
Code language: JavaScript (javascript)

function matchFallback (req) {
  const {hostname, pathname} = new URL(req.url);

  if (isImageRequest(req)) {
    const imagePath = fallbackImages.get(hostname);
    return caches.match(imagePath);
  }

  if (isPageRequest(req)) {
    const pagePath = fallbackPages.get(pathname);
    return caches.match(pagePath);
  }

  // Use an new response if nothing better can be found.
  return Promise.resolve(new Response(/* ... */));
}
Code language: JavaScript (javascript)

In the next part of this series, we’ll cover strategies for invalidating old caches and limiting the amount of storage space they can occupy.

Footnotes

  1. To fill in for the sparse browser implementation, it’s recommended to use some form of cache-busting when pre-caching static resources.  Return to the text before footnote 1
Previous Next
Copyrights
We respect the property rights of others, and are always careful not to infringe on their rights, so authors and publishing houses have the right to demand that an article or book download link be removed from the site. If you find an article or book of yours and do not agree to the posting of a download link, or you have a suggestion or complaint, write to us through the Contact Us .
Read More