Service Workers at Scale, Part II: Handling Fallback Resources
At 4/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.
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 => {
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.
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:
- Images embedded in articles
- 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)
“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']
]);
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)
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.
Footnotes
- 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