Service Workers at Scale, Part II: Handling Fallback Resources

Written by Erik Jung on

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.

Pre-caching fallback dependencies

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 => {
      .then(cache => {
        return cache.addAll([
      .catch(err => console.warn(err))

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:

  new Request('/offline', {...}),
  new Request('/books-offline', {...}),
  new Request('/assets/offline-avatar.png', {...}),
  new Request('/assets/offline-photo.png', {...})

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:

  new Request('/offline', {credentials: 'same-origin'}),
  // ...

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 fresh1:

  new Request('/assets/offline-photo.png', {cache: 'reload'}),
  // ...

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

Responding with fallback images

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'],
  ['', '/assets/offline-avatar.png']

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);

  // ...

“Redirecting” to offline pages

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']

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(/* ... */));

Coming up next: Cache trimming and invalidation

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.

  1. To fill in for the sparse browser implementation, it’s recommended to use some form of cache-busting when pre-caching static resources. 
Erik Jung

Erik Jung is a developer equally interested in design and all other aspects of building things for the web. You can follow his thoughts on CSS architecture, build tools, and emerging JavaScript trends at @erikjung.

Never miss an article!

Get Weekly Digests

Let’s discuss your project! Email Us