Strategies for Service Worker Caching for Progressive Web Apps

TL;DR

This post goes through the issues with service worker caching, strategies that can be followed to have an ideal experience with service workers and what can be done to recover from a broken deployment. Also focusses on issues with create-react-app’s opt-out behavior causing unintended side effects.

Here’s a quick summary of what this post covers:

  • Service Worker caching enables offline first apps. In case its a broken deployment you can’t quickly apply a patch because service-worker.js file itself might be cached, in which case your new deployment might not be rolled out to the end user till the original service-worker.js file’s cache is invalidated (usually ~24h).
  • create-react-app includes a service worker by default and makes your app work offline by default. (can throw up unexpected behavior if you are not aware of what is happening).
  • Set cache-control to max-age 0, no-cache as response header for service-worker.js file. Set appropriate cache headers for other cached assets.
  • Do not cache bad responses (3xx, 4xx, 5xx).
  • Use Clear-Site-Data kill switch to reset your application cache and localstorage for your users. Call unregister() for unregistering your service worker that is already live.

What is a service worker?

A service worker is a JavaScript file that runs separately from the main browser thread, intercepting network requests, caching or retrieving resources from the cache, and delivering push messages.

Here’s the lifecycle of a service worker:

The installation phase is the first one that happens and it happens once. Depending on your validity of service worker code, it could go into Activated phase or Error phase (rejected). Once activated, it will be running in the background and will wait for fetch / message events and respond to them. Finally it can be terminated (unregistered) naturally when the user closes the browser tab.

The Problem

Lets say you deployed your app with service worker(s) enabled, not knowing that service-worker.js file itself is cached. If you make a change to your service worker, then reloading the page won’t kill the old one and activate the new one (Your service worker is considered updated if it’s byte-different to the one the browser already has), leaving you somewhat confused as to why your changes haven’t taken effect. This is because the old window is never actually closed, meaning there’s never a time to swap them out — you need to kill the tab completely then reopen it.

What does this mean to your app and your users?

Reloading the page doesn’t work as expected. Users will get the cached version (worst in case its a broken deployment) of your app instead of what you intended them to see. You cannot add a new script and deploy to clear the caching mess. You need to wait out the cache expiry time (usually ~24h if no http cache header is set) to invalidate the service-worker.js file cache and to refetch a new version, unless the user decides to clear cache themselves which is a highly unlikely scenario and one that can’t be relied on. By the way, Shift + Reload will temporary skip caches to load the latest version of the app.

create-react-app service worker issues

React’s project scaffolding tool, create-react-app includes a service worker by default and this created issues for people who were not aware of what was happening. Like this example of a reddit post where the user is complaining that their react app was running despite uninstalling node, restarting pc and what not. A lot of users were just not aware or informed about this service worker addition and debugging was hard in this case.

There is a github issue in create-react-app with a discussion heading towards disabling service workers by default.

Note: Create React App includes a service worker by default as of today and they are planning to change to opt-in from react-scripts v2.0.

Another horror story example was demonstrated at last year’s JS Conf EU, where a production app was perpetually throwing errors due to a bad initial service-worker deployment.

Smart Caching

Any asset that fails to load during the installation phase will reject the entire caching registration. For example,

const PRECACHE_URLS = ['/asset.js','/asset.css'];

self.addEventListener('install', event => {

    event.waitUntil(

        caches.open(VERSION)

            .then(cache => cache.addAll(PRECACHE_URLS))

            .then(self.skipWaiting())

    );

});

If any one of cache.addAll(PRECACHE_URLS) fails, then its a rejection and none of them will be cached.

Do not cache certain pages to avoid weird behavior. For example, if you are caching your login page and if the user goes offline after logging in and the client side logic redirects to /login on network failure, the user will be presented the login page offline (not ideal user experience)

Do not cache 3xx redirect response pages. Do not cache bad responses 4xx or 5xx. All of these will result in unintended side effects and will be bad UX.

Pre-deployment strategy

Assuming you haven’t deployed service worker before, to avoid all the above headaches and horror stories, you need to make sure that requests to service-worker.js is not cached.

Use the following cache-control policy for service-worker.js file.

cache-control: max-age=0,no-cache,no-store,must-revalidate

This response header can be set in

  • Server side nginx location directives
location ~* (service-worker\.js)$ {    
    add_header 'Cache-Control' 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';    
    expires off;    
    proxy_no_cache 1;
}
  • Server side express.js response headers
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0');

and if you are using a CDN, apply the setting to use origin cache headers.

A banner showing that “You are currently offline” message should also be implemented so that users are aware whenever they are browsing your app offline.

Here’s a simple offline message example by Google Chrome Devs that can be implemented in your service worker.

Always keep clearing old cached versions of your assets to free up memory for your users and also to not cross storage limits. Addy Osmani has a good post about storage limits for different browsers/devices in a PWA app context.

Post deployment strategy

Ok, I have already deployed my service worker (knowingly/unknowingly) and it seems to be broken. What do I do now?

Kill Switch

There is a W3C spec with clear-site-data header that can be used as a kill switch to clear your site cookies, cache etc. You can add this header and deploy to clear all.

Clear-Site-Data: cookies, cache

The goal of the kill switch is to terminate and deregister service workers registered for an origin. You will still need to wait for expiry of the HTTP cache header (~24h if nothing is set) for the original cache invalidation. You can add this header in the same way we discussed above (nginx directive or express.js response headers).

Unregister service worker

You can remove all registered service workers in your next update by calling the unregister() method. The following piece of code will be inside your service worker:

navigator.serviceWorker.getRegistrations().then(function(registrations) {
 for(let registration of registrations) {
  registration.unregister()
} })

You can also have a service worker check for a newer version and do a force update. Don’t forget to apply the pre-deployment strategies mentioned above.

Problems aside, service workers are still a good way to manage cache for apps if used smartly. There are a lot of apps running in production with service workers enabled. If you are using Chrome, you can use this internal URL to look at the apps you visited that are running service worker.

chrome://serviceworker-internals/

Blog
15 Mar, 2018
Email
Subscribe to stay up-to-date on all things Hasura. One newsletter, once a month.
Loading...
v3-pattern
Accelerate development and data access with radically reduced complexity.