Service Workers

A Service Worker is a request broker within the browser with the potential to intercept any HTTP request and respond with content of its choice. Typically it will respond with a previously cached response. You can also precache an array of named resources during the initial page load. By caching html, JavaScript, or other resources we can make subsequent page loads very fast.

Note: The Service Worker cache is a distinct cache, unrelated to disk cache, memory cache, local storage or session storage.

Fusion.js Service Worker Plugin

Fusion.js comes with an optional Service Worker plugin which provides basic html and service worker caching out of the box. As a web app developer you are only responsible for registering the plugin and providing a Service Worker file at src/sw.js.

Providing the Service Worker file

The Service Worker plugin includes event handlers to make it easy to define a default Service Worker file:

import {getHandlers} from "fusion-plugin-service-worker";

export default (assetInfo) => {
  const {onFetch, onInstall} = getHandlers(assetInfo);
  self.addEventListener("install", onInstall);
  self.addEventListener("fetch", onFetch);
}

You're free to provide a custom file instead. Fusion's standard ES2015+ support applies.

The assetInfo argument is an object of the form {precachePaths, cacheablePaths}.

  • precachePaths is an array of paths to resources to be cached on first load,
  • cacheablePaths is an array of paths to resources to be lazily cached after their first http fetch. \

The assetInfo data is automatically generated by the build and passed to src/sw.js via the Service Worker plugin.

Registering the plugin

The fusion-cli build process auto-generates an swTemplateFunction. The body of this function will be the transpiled code from src/sw.js and its argument will be the assetInfo object (see above). Register this function as the value of SWTemplateFunctionToken (node only).

// src/main.js
import App from 'fusion-react';

import SwPlugin, {SWTemplateFunctionToken} from 'fusion-plugin-service-worker';

app.register(SWTemplateFunctionToken, swTemplateFunction);
app.register(SwPlugin);
if (__NODE__) {
  app.register(SWTemplateFunctionToken, swTemplateFunction);
}

Service Worker registration

The Service Worker plugin will try to register a Service Worker on every page load. If there's already a registered ServiceWorker for this domain, subsequent registration attempts are ignored, unless at least one byte of src/sw.js has changed. (To ensure the Service Worker is always re-intalled after a new build, we also update a timestamp argument in swTemplateFunction on each build). If the registration is succesful the install handler is called, followed by the activate handler.

More about the handlers

install: Since this handler should only called after a new build, this is usually a good place to clear existing resource caches. The Service Worker plugin's default install handler invalidates the cache for any outdated resources. This also where we cache the precachePaths.

activate: Depending on the implementation of the install handler, the activate handler is either called immediately after install or after all tabs using the old Service Worker have been closed. Our Service Worker plugin's default install handler calls self.skipWaiting() which means activate is called immediately, so there's no need for a default activate handler in this case.

fetch: This handler gets invoked on every http request and provides an opportunity to override regular http request behavior. The Service Worker plugin's default handler will do one of three things:
a) If the resouce url does not match cacheablePaths (and the request is not for HTML), allow the request to continue through to the server where it will be processed in the normal way.
b) If a response for that url is already in the Service Worker cache, immediately return the cached repsonse.
c)if the resouce is not yet in the Service Worker cache, fetch the response from the server and cache a clone of the response value on the way back.

Things to know

Viewing the Service Worker Cache

In Chrome developer tools go to Application tab > Cache Storage and open the drop down. If you're using the Service Worker plugin default handlers, your cache key should always be '0.0.0'. Sometimes you'll need to click refresh from the context menu to show the current cache. To delete a cache entry, click delete on the cache entry's context menu

Disabling Service Worker

Sometimes, particularly in development, you'll want to disable the Service Worker.\

In Chrome you can do this via developer tools: Application tab > Service Workers > Check "Bypass for Network". Other browsers have similar opt-outs.

Cross-app caching in development

If you view multiple websites on the same localhost port (e.g localhost:3000) the browser will try to apply the Service Worker from a previous website to your current website. You can either disable the Service Worker (see above) or delete the cache (also above) and make sure you unregister any old Service Workers (go to Application tab > Service Workers).

When intercepting an HTML request, make sure you only cache genuine HTML responses

Sometimes an HTML response will be an error or a redirect. If you cache such a response you will no longer be able to access other resources (including subsequent Service Worker updates) and the app will be effectively frozen.

The Service Worker plugin's default fetch handler ensures only genuine HTML responses are cached.

Here's some logic you can use if you roll your own custom src/sw.js file:

function responseIsHtml(response) {
  if (!response || !response.headers) {
    return false;
  }
  const contentType = response.headers.get('content-type');
  return contentType && contentType.indexOf('html') > -1;
}

For extra security, the default fetch handler will also delete the cached HTML after one day.

Why do we cache HTML?

The browser can't begin to fetch JavaScript, image and other resources until the HTML has been fetched. Using a cached HTML response will eliminate significant fetch latency, particularly over slow networks.

My Service Worker is breaking things. How do I unregister it across all clients?

If you need all users to unregister the Service Worker, you can register the SWRegisterToken with the value false. Make sure you only register this token in the browser.

if (__BROWSER__) {
  app.register(SWRegisterToken, false);
}

Does my app need a Service Worker?

Not necessarily, that's why this plugin is opt-in. We recommend adding the Service Worker plugin where performance is a premium user concern, and especially if your app is likely to be used worldwide by users with a variety of network conditions and devices. (Note that on low-level devices, regular disk cache retrieval can be extremely slow compared to Service Worker cache retrieval).

For more details see the README.