Missed the action at the 2018 Chrome Dev Summit? Catch up with our playlist on the Google Chrome Developers channel on YouTube. Watch now.

Advanced Recipes

Offer a page reload for users

A common UX pattern for progressive web apps is to show a banner when a service worker has updated and waiting to install.

To do this you'll need to add some code to your page and to your service worker.

Add to your page

function showRefreshUI(registration) {
  // TODO: Display a toast or refresh UI.

  // This demo creates and injects a button.

  var button = document.createElement('button');
  button.style.position = 'absolute';
  button.style.bottom = '24px';
  button.style.left = '24px';
  button.textContent = 'This site has updated. Please click to see changes.';

  button.addEventListener('click', function() {
    if (!registration.waiting) {
      // Just to ensure registration.waiting is available before
      // calling postMessage()

    button.disabled = true;



function onNewServiceWorker(registration, callback) {
  if (registration.waiting) {
    // SW is waiting to activate. Can occur if multiple clients open and
    // one of the clients is refreshed.
    return callback();

  function listenInstalledStateChange() {
    registration.installing.addEventListener('statechange', function(event) {
      if (event.target.state === 'installed') {
        // A new service worker is available, inform the user

  if (registration.installing) {
    return listenInstalledStateChange();

  // We are currently controlled so a new SW may be found...
  // Add a listener in case a new SW is found,
  registration.addEventListener('updatefound', listenInstalledStateChange);

window.addEventListener('load', function() {
  .then(function (registration) {
      // Track updates to the Service Worker.
    if (!navigator.serviceWorker.controller) {
      // The window client isn't currently controlled so it's a new service
      // worker that will activate immediately

    // When the user asks to refresh the UI, we'll need to reload the window
    var preventDevToolsReloadLoop;
    navigator.serviceWorker.addEventListener('controllerchange', function(event) {
      // Ensure refresh is only called once.
      // This works around a bug in "force update on reload".
      if (preventDevToolsReloadLoop) return;
      preventDevToolsReloadLoop = true;
      console.log('Controller loaded');

    onNewServiceWorker(registration, function() {

This code handles the various possible lifecycles of the service worker and detects when a new service worker has become installed and is waiting to activate.

When a waiting service worker is found we set up a 'controllerchange' listener on navigator.serviceWorker so we know when to reload the window. When the user clicks on the UI to refresh the page, we post a message to the new service worker telling it to skipWaiting meaning it'll start to activate.

Add to your service worker

self.addEventListener('message', (event) => {
  if (!event.data){

  switch (event.data) {
    case 'skipWaiting':
      // NOOP

This will receive a the 'skipWaiting' message and call skipWaiting()forcing the service worker to activate immediately.

"Warm" the runtime cache

After configuring some routes to manage caching of assets, you may want to add some files to the cache during the service worker installation.

To do this you'll need to install your desired assets to the runtime cache.

self.addEventListener('install', (event) => {
  const urls = [/* ... */];
  const cacheName = workbox.core.cacheNames.runtime;
  event.waitUntil(caches.open(cacheName).then((cache) => cache.addAll(urls)));

If you setup routes with a custom cachename you can do the same, just replace the cacheName with your custom value.

Provide a fallback response to a route

There are scenarios where returning a fallback response is better than failing to return a response at all. An example is returning a placeholder image when the original image can't be retrieved.

To do this in all versions of Workbox you can use the handle() method on strategy to make a custom handler function. Note: You should precache any assets you use for your fallback; in the example below we'd need to make sure that FALLBACK_IMAGE_URL was already cached.

const FALLBACK_IMAGE_URL = '/images/fallback.png';

  new RegExp('/images/'),
  async ({event}) => {
    try {
      return await workbox.strategies.cacheFirst().handle({event});
    } catch (error) {
      return caches.match(FALLBACK_IMAGE_URL);

Starting in Workbox v4, all of the built-in caching strategies reject in a consistent manner when there's a network failure and/or a cache miss. This promotes the pattern of setting a global "catch" handler to deal with any failures in a single handler function:

// Use an explicit cache-first strategy and a dedicated cache for images.
  new RegExp('/images/'),
    cacheName: 'images',
    plugins: [...],

// Use a stale-while-revalidate strategy for all other requests.

// This "catch" handler is triggered when any of the other routes fail to
// generate a response.
workbox.routing.setCatchHandler(({event}) => {
  // Use event, request, and url to figure out how to respond.
  // One approach would be to use request.destination, see
  // https://medium.com/dev-channel/service-worker-caching-strategies-based-on-request-types-57411dd7652c
  switch (event.request.destination) {
    case 'document':
      return caches.match(FALLBACK_HTML_URL);

    case 'image':
      return caches.match(FALLBACK_IMAGE_URL);

    case 'font':
      return caches.match(FALLBACK_FONT_URL);

      // If we don't have a fallback, just return an error response.
      return Response.error();

Use postMessage() to notify of cache updates

On browsers which lack Broadcast Channel API support, the workbox-broadcast-cache-update plugin will not send any messages letting clients know that a cached response was updated.

As an alternative to using workbox-broadcast-cache-update, you can instead "roll your own" plugin which listens for the same cache update events, and uses the more widely supported postMessage() API for sending out updates. In this custom plugin, you have control over what criteria are used to determine whether a cached response has been updated. You can use whatever message format inside of postMessage() that makes sense for your use case.

Here's an example of one possible implementation:

const postMessagePlugin = {
  cacheDidUpdate: async ({cacheName, url, oldResponse, newResponse}) => {
    // Use whatever logic you want to determine whether the responses differ.
    if (oldResponse && (oldResponse.headers.get('etag') !== newResponse.headers.get('etag'))) {
      const clients = await self.clients.matchAll();
      for (const client of clients) {
        // Use whatever message body makes the most sense.
        // Note that `Response` objects can't be serialized.
        client.postMessage({url, cacheName});

// Later, use the plugin when creating a response strategy:
  new RegExp('/path/prefix'),
    plugins: [postMessagePlugin],

Make standalone requests using a strategy

Most developers will use one of Workbox's strategies as part of a router configuration. This setup makes it easy to automatically respond to specific fetch events with a response obtained from the strategy.

There are situations where you may want to use a strategy in your own router setup, or instead of a plain fetch() request.

To help with these sort of use cases, you can use any of the Workbox strategies in a "standalone" fashion via the makeRequest() method.

// Inside your service worker code:
const strategy = workbox.strategies.networkFirst({
  networkTimeoutSeconds: 10,
const response = await strategy.makeRequest({
  request: 'https://example.com/path/to/file',
// Do something with response.

The request parameter is required, and can either be a Request object or a string representing a URL.

The event parameter is an optional ExtendableEvent. If provided, it will be used to keep the service worker alive (via event.waitUntil()) long enough to complete any "background" cache updates and cleanup.

makeRequest() returns a promise for a Response object.

You can use it in a more complex example as follows:

self.addEventListener('fetch', async (event) => {
  if (event.request.url.endsWith('/complexRequest')) {
    // Configure the strategy in advance.
    const strategy = workbox.strategies.staleWhileRevalidate({cacheName: 'api-cache'});

    // Make two requests using the strategy.
    // Because we're passing in event, event.waitUntil() will be called automatically.
    const firstPromise = strategy.makeRequest({event, request: 'https://example.com/api1'});
    const secondPromise = strategy.makeRequest({event, request: 'https://example.com/api2'});

    const [firstResponse, secondResponse] = await Promise.all(firstPromise, secondPromise);
    const [firstBody, secondBody] = await Promise.all(firstResponse.text(), secondResponse.text());

    // Assume that we just want to concatenate the first API response with the second to create the
    // final response HTML.
    const compositeResponse = new Response(firstBody + secondBody, {
      headers: {'content-type': 'text/html'},


Serve cached audio and video

There are a few wrinkles in how some browsers request media assets (e.g., the src of a <video> or <audio> element) that can lead to incorrect serving behavior unless you take specific steps when configuring Workbox.

Full details are available in this GitHub issue discussion; a summary of the important points is:

  • Workbox must be told to respect Range request headers by adding in the workbox-range-requests plugin to the strategy used as the handler.
  • The audio or video element needs to opt-in to CORS mode using the crossOrigin attribute, e.g. via <video src="movie.mp4" crossOrigin="anonymous"></video>.
  • If you want to serve the media from the cache, you should explicitly add it to the cache ahead of time. This could happen either via precaching, or via calling cache.add() directly. Using a runtime caching strategy to add the media file to the cache implicitly is not likely to work, since at runtime, only partial content is fetched from the network via a Range request.

Putting this all together, here's an example of one approach to serving cached media content using Workbox:

<!-- In your page: -->
<!-- You currently need to set crossOrigin even for same-origin URLs! -->
<video src="movie.mp4" crossOrigin="anonymous"></video>
// In your service worker:
// It's up to you to either precache or explicitly call cache.add('movie.mp4')
// to populate the cache.
// This route will go against the network if there isn't a cache match,
// but it won't populate the cache at runtime.
// If there is a cache match, then it will properly serve partial responses.
    cacheName: 'your-cache-name-here',
    plugins: [
      new workbox.cacheableResponse.Plugin({statuses: [200]}),
      new workbox.rangeRequests.Plugin(),