Workbox: Allow specifying specific strategies for precaching

Created on 27 Nov 2018  路  7Comments  路  Source: GoogleChrome/workbox

Library Affected:
workbox-precaching

Browser & Platform:
All

Issue or Feature Request Description:
By default precaching uses a bespoke cache-first strategy. This is the right strategy most of the time, but not all the time. See discussion here: https://twitter.com/jeffposnick/status/1067081651803619328.

It is possible to work around this right now, but only by creating routes from scratch and configuring them to use the precache cache directly, and doing so can silently cause problems and break things with some strategies.

It would be useful to be able to pass a strategy into precache route setup directly. With that in place, workbox could:

  • Allow new combinations of existing strategies and precaching options
  • Allow custom strategies and other advanced fine tuning of precaching
  • Catch & warn users directly when trying to set up known bad combinations of strategies (e.g. stale-while-revalidate of precached revisioned resources)
Feature Request workbox-precaching

Most helpful comment

I've found an almost perfect (but still not suitable) service worker code for my case:

// The ordinary SW stuff here like importScripts, self.addEventListener, etc.

// self.__precacheManifest includes index.html
workbox.precaching.precacheAndRoute(self.__precacheManifest, {
  directoryIndex: '', // To make the code below works as expected at the root path
});

// Implements the network-first strategy for navigation routes that falls back to preloaded cache (index.html)
workbox.routing.registerRoute(
  new workbox.routing.NavigationRoute(async ({ url }) => {
    const cacheName = workbox.core.cacheNames.precache;
    const cacheKey = workbox.precaching.getCacheKeyForURL('/index.html');
    const cache = await caches.open(cacheName);

    let networkResponse;
    let networkError;
    try {
      networkResponse = await fetch(url);
      if (networkResponse.ok) {
        await cache.put(cacheKey, networkResponse.clone());
        return networkResponse;
      }
    } catch (error) {
      networkError = error;
    }

    const cacheMatch = await cache.match(cacheKey);
    if (cacheMatch) {
      return cacheMatch;
    } else if (networkResponse) {
      return networkResponse;
    } else {
      throw networkError;
    }
  })
);

// Don't add `workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("/index.html"));`

The lack is that when I reload the updated application, then go offline and reload the page again, my new HTML file is used but Workbox doesn't load the new versions of assets (added through precacheAndRoute) from the cache until all the tabs are closed. It ruins the whole solution.

image

I suggest that the Workbox precaching module is built with 芦apply update when tabs closed禄 strategy and I can't mix strategies: either obey or write your own precaching mechanism.

It would be great if Workbox has an option to use the network first strategy for precaching.

All 7 comments

Assigning this to @philipwalton for the latest thinking. I know that he's been looking into this space, but the ultimate resolution might just be not to proceed with this feature request.

(For the record, I am of the opinion that allowing folks to swap in alternative strategies for precaching is a Bad Idea, and that the best approach for folks who want something other than cache-first behavior is to runtime caching instead.)

Personally, I would love to have online first strategy for precaching. For someone like me who is only just getting into service workers, it would feel much safer to only rely on the cache when offline, but otherwise run normally. Eventually I would probably switch to offline first anyway, but the onlinefirst mode can be an important step in between allowing people to feel more confident about going fully offline first.

I don't think you should use workbox-precaching given that use case.

You can accomplish that by setting up a runtime caching route that uses a network-first strategy, and if needed, calling cache.addAll() inside of an install handler to pre-populate URLs.

workbox-precaching really only works if the source of truth about the current version of each cache entry is reflected in the current precache manifest.

My use-case is to:

  • Cache the _whole_ application on the first launch so that it's available offline
  • Make the application update itself _ASAP_ on the next online launch (like if the application doesn't use a service worker at all)
  • Have a single SPA shell that serves all the navigation routes (the server is configured for it)

The default strategy implemented by GenerateSW Webpack loader is to precacheAndRoute all the emitted files. It's not acceptable because it requires users to close all the application tabs to apply the update.

I can not use runtime caching with NetworkFirst strategy instead of precacheAndRoute because not all of the application assets are requested on application launch. Also there are many navigation routes that must be cached too.

Actually, I need the network first strategy only for /index.html because all the other assets have version hash in their paths. I tried to exclude it from the precacheAndRoute entries list, but it broke registerNavigationRoute that uses index.html as the SPA shell.

This solution isn't suitable too because the cache is stored for each individual navigation route (instead of a single cache like required for SPA shell). Also index.html is cached only when you open the page for the 2nd time.

@jeffposnick How can I configure GenerateSW or InjectManifest to implement network-first strategy for index.html while keeping it as the SPA shell?

I've found an almost perfect (but still not suitable) service worker code for my case:

// The ordinary SW stuff here like importScripts, self.addEventListener, etc.

// self.__precacheManifest includes index.html
workbox.precaching.precacheAndRoute(self.__precacheManifest, {
  directoryIndex: '', // To make the code below works as expected at the root path
});

// Implements the network-first strategy for navigation routes that falls back to preloaded cache (index.html)
workbox.routing.registerRoute(
  new workbox.routing.NavigationRoute(async ({ url }) => {
    const cacheName = workbox.core.cacheNames.precache;
    const cacheKey = workbox.precaching.getCacheKeyForURL('/index.html');
    const cache = await caches.open(cacheName);

    let networkResponse;
    let networkError;
    try {
      networkResponse = await fetch(url);
      if (networkResponse.ok) {
        await cache.put(cacheKey, networkResponse.clone());
        return networkResponse;
      }
    } catch (error) {
      networkError = error;
    }

    const cacheMatch = await cache.match(cacheKey);
    if (cacheMatch) {
      return cacheMatch;
    } else if (networkResponse) {
      return networkResponse;
    } else {
      throw networkError;
    }
  })
);

// Don't add `workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("/index.html"));`

The lack is that when I reload the updated application, then go offline and reload the page again, my new HTML file is used but Workbox doesn't load the new versions of assets (added through precacheAndRoute) from the cache until all the tabs are closed. It ruins the whole solution.

image

I suggest that the Workbox precaching module is built with 芦apply update when tabs closed禄 strategy and I can't mix strategies: either obey or write your own precaching mechanism.

It would be great if Workbox has an option to use the network first strategy for precaching.

I've been talking to @philipwalton about this for v6, as he's been planning on a rewrite of the workbox-precaching internals.

The main takeaway has been that it's _not_ safe to allow developers to override the default Strategy subclass that will be used for handling caching. Doing so would really open the door for using workbox-precaching in a way that leaves your cache state inconsistent and didn't play nicely with the service worker install/activate events. Instead, what we're likely to do is just allow a "safe" subset of plugins to modify precaching's Strategy.

There are legitimate use cases for pre-populating a cache while using a non-cache-first Strategy to serve those requests. For those cases, we recommend calling cache.addAll() inside of an install handler, and then using workbox-routing to associate appropriate Strategys with different routing conditions. We will likely publish a "recipe" in the docs to formalize this recommendation, but in the meantime, something like the following should allow you to do this while still using the self.__WB_MANIFEST injection from our build tools if you want鈥攐r you can just generate your list of URLs via other build methods if you'd like.

import {registerRoute} from 'workbox-routing';

const cacheName = 'install-cache';
// This assumes you're using injectManifest to replace the variable.
const manifest = self.__WB_MANIFEST;
// We just need the url, not the revision, since managing cache
// revisions will happen via runtime caching updates.
// Put this variable in the top-level scope so that we can use
// it later on in routing.
const manifestURLs = manifest.map((entry) => {
  // Create a full, absolute URL to make routing easier.
  const url = new URL(entry.url, self.location);
  return url.href;
});

// This takes care of pre-populating the cache.
// Whenever the manifest changes, it will be re-run, but unlike with precaching,
// there is no automatic "clean up".
self.addEventListener('install', (event) => {
  const populateCache = async () => {
    const cache = await caches.open(cacheName);
    await cache.addAll(manifestURLs);
  };

  event.waitUntil(populateCache());
});

// Register one or more runtime routes as needed.
// This is just an example.
registerRoute(
  // If this is a URL in the manifest...
  ({url}) => manifestURLs.includes(url.href),
  // ...respond using a StaleWhileRevalidate strategy.
  new StaleWhileRevalidate({cacheName}),
);

Given that, I'm going to close this issue.

Just sharing a little hack I made to force network-first for index.html:

// sw.js
import { precacheAndRoute } from 'workbox-precaching'
import { registerRoute } from 'workbox-routing'
import { skipWaiting, clientsClaim } from 'workbox-core'
import navigationRoute from './js/sw/navigation'

precacheAndRoute([
  // for some reason, it's an object and precacheAndRoute complains when it's not converted to array
  ...self.__WB_MANIFEST
])

registerRoute(navigationRoute())

skipWaiting()
clientsClaim()
// navigation.js
import { cacheNames } from 'workbox-core'
import { NavigationRoute } from 'workbox-routing'
import { NetworkFirst } from 'workbox-strategies'

export default function() {
  const networkFirst = new NetworkFirst({
    cacheName: cacheNames.precache
  })
  return new NavigationRoute(options => {
    options.request = new Request('/index.html')
    return networkFirst.handle(options)
  })
}

I don't know what are the gotchas yet for this hack. If anyone knows or encounters some, I'd love to know as well.

Was this page helpful?
0 / 5 - 0 ratings