The Workbox logo.

Extending Workbox

Extending Workbox

Write your own reusable, shareable strategies and plugins.

Appears in: Network reliability

In this article, we're going to take a quick tour of some ways of extending Workbox. By the end, you'll be writing your own strategies and plugins, and hopefully sharing them with the world.

If you're more of a visual person, you can watch a recording of a Chrome Dev Summit talk covering the same material:

What's Workbox? #

At its core, Workbox is a set of libraries to help with common service worker caching scenarios. And when we've written about Workbox in the past, the emphasis has been on "common" scenarios. For most developers, the caching strategies that Workbox already provides will handle your caching needs.

The built-in strategies include stale-while-revalidate, where a cached response is used to respond to a request immediately, while the cache is also updated so that it's fresh the next time around. They also include network-first, falling back to the cache when the network is unavailable, and a few more.

Custom strategies #

But what if you wanted to go beyond those common caching scenarios? Let's cover writing your own custom caching strategies. Workbox v6 offers a new Strategy base class that sits in front of lower-level APIs, like Fetch and Cache Storage. You can extend the Strategy base class, and then implement your own logic in the _handle() method.

Handle simultaneous, duplicate requests with DedupeNetworkFirst #

For instance, imagine that you want to implement a strategy that can handle multiple, simultaneous requests for the same URL by deduplicating them. A copy of the response is then used to fulfill all of the in-flight requests, saving bandwidth that would otherwise be wasted.

Here's the code you can use to implement that, by extending the NetworkFirst strategy (which itself extends the Strategy base):

// See https://developers.google.com/web/tools/workbox/guides/using-bundlers
import {NetworkFirst} from 'workbox-strategies';

class DedupeNetworkFirst extends NetworkFirst {
constructor(options) {
super(options);
// This maps inflight requests to response promises.
this._requests = new Map();
}

// _handle is the standard entry point for our logic.
async _handle(request, handler) {
let responsePromise = this._requests.get(request.url);

if (responsePromise) {
// If there's already an inflight request, return a copy
// of the eventual response.
const response = await responsePromise;
return response.clone();
} else {
// If there isn't already an inflight request, then use
// the _handle() method of NetworkFirst to kick one off.
responsePromise = super._handle(request, handler);
this._requests.set(request.url, responsePromise);
try {
const response = await responsePromise;
return response.clone();
} finally {
// Make sure to clean up after a batch of inflight
// requests are fulfilled!
this._requests.delete(request.url);
}
}
}
}

This code assumes that all requests for the same URL can be satisfied with the same response, which won't always be the case if cookies or session state information comes into play.

Create a race between the cache and network with CacheNetworkRace #

Here's another example of a custom strategy—one that's a twist on stale-while-revalidate, where both the network and cache are checked at the same time, with a race to see which will return a response first.

// See https://developers.google.com/web/tools/workbox/guides/using-bundlers
import {Strategy} from 'workbox-strategies';

// Instead of extending an existing strategy,
// this extends the generic Strategy base class.
class CacheNetworkRace extends Strategy {
// _handle is the standard entry point for our logic.
_handle(request, handler) {
// handler is an instance of the StrategyHandler class,
// and exposes helper methods for interacting with the
// cache and network.
const fetchDone = handler.fetchAndCachePut(request);
const matchDone = handler.cacheMatch(request);

// The actual response generation logic relies on a "race"
// between the network and cache promises.
return new Promise((resolve, reject) => {
fetchDone.then(resolve);
matchDone.then((response) => response && resolve(response));

// Promise.allSettled() is implemented in recent browsers.
Promise.allSettled([fetchDone, matchDone]).then(results => {
if (results[0].status === 'rejected' &&
!results[1].value) {
reject(results[0].reason);
}
});
});
}
}

Although it's not required, it's strongly recommended that when interacting with the network or cache, you use the instance of the StrategyHandler class that's passed to your _handle() method. It's the second parameter, called handler in the example code.

This StrategyHandler instance will automatically pick up the cache name you've configured for the strategy, and calling its methods will invoke the expected plugin lifecycle callbacks that we'll describe soon.

A StrategyHandler instance supports the following methods:

MethodPurpose
fetchCalls fetch(), invokes lifecycle events.
cachePutCalls cache.put() on the configured cache, invokes lifecycle events.
cacheMatchCalls cache.match() on the configured cache, invokes lifecycle events.
fetchAndCachePutCalls fetch() and then cache.put() on the configured cache, invokes lifecycle events.

Drop-in support for routing #

Writing a Workbox strategy class is a great way to package up response logic in a reusable, and shareable, form. But once you've written one, how do you use it within your larger Workbox service worker? That's the best part—you can drop any of these strategies directly into your existing Workbox routing rules, just like any of the "official" strategies.

// See https://developers.google.com/web/tools/workbox/guides/using-bundlers
import {ExpirationPlugin} from 'workbox-expiration';
import {registerRoute} from 'workbox-routing';

// DedupeNetworkFirst can be defined inline, or imported.

registerRoute(
({url}) => url.pathname.startsWith('/api'),
// DedupeNetworkFirst supports the standard strategy
// configuration options, like cacheName and plugins.
new DedupeNetworkFirst({
cacheName: 'my-cache',
plugins: [
new ExpirationPlugin({...}),
]
})
);

A properly written strategy should automatically work with all plugins as well. This applies to the standard plugins that Workbox provides, like the one that handles cache expiration. But you're not limited to using the standard set of plugins! Another great way to extend Workbox is to write your own reusable plugins.

Custom plugins #

Taking a step back, what is a Workbox plugin, and why would you write your own? A plugin doesn't fundamentally change the order of network and cache operations performed by a strategy. Instead, it allows you to add in extra code that will be run at critical points in the lifetime of a request, like when a network request fails, or when a cached response is about to be returned to the page.

Lifecycle event overview #

Here's an overview of all the events that a plugin could listen to. Technical details about implementing callbacks for these events is in the Workbox documentation.

Lifecycle EventPurpose
cacheWillUpdateChange response before it's written to cache.
cacheDidUpdateDo something following a cache write.
cacheKeyWillBeUsedOverride the cache key used for reads or writes.
cachedResponseWillBeUsedChange response read from cache before it's used.
requestWillFetchChange request before it's sent to the network.
fetchDidFailDo something when a network request fails.
fetchDidSucceedDo something when a network request succeeds.
handlerWillStartTake note of when a handler starts up.
handlerWillRespondTake note of when a handler is about to respond.
handlerDidRespondTake note of when a handler finishes responding.
handlerDidCompleteTake note of when a handler has run all its code.
handlerDidErrorProvide a fallback response if a handler throws an error.

When writing your own plugin, you'll only implement callbacks for the limited number of events that match your purpose—there's no need to add in callbacks for all of the possible events. Additionally, it's up to you whether you implement your plugin as an Object with properties that match the lifecycle event names, or as a class that exposes methods with those names.

Lifecycle events example: FallbackOnErrorPlugin #

For instance, here's a custom plugin class that implements callback methods for two events: fetchDidSucceed, and handlerDidError.

class FallbackOnErrorPlugin {
constructor(fallbackURL) {
// Pass in a URL that you know is cached.
this.fallbackURL = fallbackURL;
}

fetchDidSucceed({response}) {
// If the network request returned a 2xx response,
// just use it as-is.
if (response.ok) {
return response;
};

// Otherwise, throw an error to trigger handlerDidError.
throw new Error(`Error response (${response.status})`);
}

// Invoked whenever the strategy throws an error during handling.
handlerDidError() {
// This will match the cached URL regardless of whether
// there's any query parameters, i.e. those added
// by Workbox precaching.
return caches.match(this.fallbackURL, {
ignoreSearch: true,
});
}
}

This plugin class provides a "fallback" whenever a strategy would otherwise generate an error response. It can be added to any strategy class, and if running that strategy does not result in a 2xx OK response, it will use a backup response from the cache instead.

Custom strategy or custom plugin? #

Now that you know more about custom strategies and plugins, you might be wondering which one to write for a given use case.

A good rule of thumb is to sketch out a diagram of your desired request and response flow, taking into account the network and cache interactions. Then, compare that to the diagrams of the built-in strategies. If your diagram has a set of connections then that's fundamentally different, that's a sign that a custom strategy is the best solution.

Conversely, if your diagram ends up looking mostly like a standard strategy but with a few extra pieces of logic injected at keys points, then you should probably write a custom plugin.

Takeaways #

Whichever approach to customizing Workbox you go with, I hope this article has inspired you write your own strategies and plugins, and then release them on npm, tagged with workbox-strategy or workbox-plugin.

Using those tags, you can search npm for strategies and plugins that have already been released.

Go out there and extend Workbox, and then share what you build!

Last updated: Improve article