Before getting started, I should point out a couple of things for you to take into account.

Today, ServiceWorker has browser support in Google Chrome, Opera, and in Firefox behind a configuration flag. Microsoft is likely to work on it soon, and there’s no official word from Apple’s Safari yet. Given that the implementation status for ServiceWorker across browsers is not great yet, it’s a great opportunity to get ahead of the pack. You won’t affect unsupported users and the ones that are supported are going to greatly appreciate it. The effectiveness of implementing ServiceWorker depends on your user-base as well, given that if most of your users are on Chrome then you won’t have to worry about support that much.

ServiceWorker is a progressive technology, and in this article I’ll show you how to take a website and make it available offline for humans who are using a modern browser while leaving humans with unsupported browsers unaffected.

Secure Connections Only

You should know that there’s a few hard requirements when it comes to ServiceWorker. First and foremost, your site needs to be served over a secure connection. If you’re still serving your site over HTTP, it might be a good excuse to implement HTTPS.

ponyfoo.com, being served over HTTPS

You could use a CDN proxy like CloudFlare to serve traffic securely. Remember to find and fix mixed content warnings as some browsers may warn your customers about your site being unsafe, otherwise.

I wrote a tutorial that may help you set up CloudFlare for your site

You might want to leverage LetsEncrypt.org to get a free TLS certificate

While the spec for HTTP/2 doesn’t inherently enforce encrypted connections, browsers intend to implement HTTP/2 and similar technologies only over TLS. The ServiceWorker specification, on the other hand, recommends browser implementation over TLS. Browsers have also hinted at marking sites served over unencrypted connections as insecure. Search engines penalize unencrypted results.

In any case, the “HTTPS only” is the implementors way of saying “this is important, you should do this”.

A Promise Based API

The future of web browser API implementations is Promise -heavy. The fetch API, for example, sprinkles sweet Promise -based sugar on top of XMLHttpRequest . ServiceWorker makes occasional use of fetch , but there’s also worker registration, caching, and message passing – all promise based.

I wrote a tutorial that may help you get started with Promises

There’s also an ES6 overview in bullet points

There’s also this tool I wrote that helps you visualize promises if you’re more of a visual learner

A screenshot of the Promisees playground

You may not be a fan of promises, but they’re here to stay, so you better get used to them.

Registering Your First ServiceWorker

I worked together with Chris on the simplest possible practical demonstration of how to use ServiceWorker. He implemented a fancy website and asked me to add offline support. I felt like that’d be a great opportunity to display how easy and unobtrusive it is to add offline capabilities to an existing website.

If you’d like to skip to the end, take a look at the Pull Request to Chris’s site on GitHub.

The first step is to register the ServiceWorker. Instead of blindly attempting the registration, we feature-detect that ServiceWorker is indeed available. The following piece of code demonstrates how we would install a ServiceWorker. The JavaScript resource passed to .register will be executed in the context of a ServiceWorker. Note how registration returns a Promise so that you can track whether or not the ServiceWorker registration was successful. I preceded logging statements with CLIENT: to make it visually easier for me to figure out whether a logging statement was coming from a web page or the ServiceWorker script.

if ( 'serviceWorker' in navigator ) { console .log( 'CLIENT: service worker registration in progress.' ); navigator.serviceWorker .register( '/service-worker.js' ).then ( function () { console .log( 'CLIENT: service worker registration complete.' ); }, function () { console .log( 'CLIENT: service worker registration failure.' ); }); } else { console .log( 'CLIENT: service worker is not supported.' ); }

The endpoint to the service-worker.js file is quite important. If the script were served from, say, /js/service-worker.js then the ServiceWorker would only be able to intercept requests in the /js/ context, but it’d be blind to resources like /other . This is typically an issue because you usually scope your JavaScript files in a /js/ , /public/ , /assets/ , or similar “directory”, whereas you’ll want to serve the ServiceWorker script from the domain root in most cases.

That was, in fact, the only necessary change to your web application code, provided that you had already implemented HTTPS. At this point, supporting browsers will issue a request for /service-worker.js and attempt to install the worker.

How should you structure the service-worker.js file, then?

Putting Together A ServiceWorker

ServiceWorker is event-driven and your code should aim to be stateless. That’s because when a ServiceWorker isn’t being used it’s shut down, losing all state. You have no control over that, so it’s best to avoid any long-term dependance on in-memory state.

Below, I listed the most notable events you’ll have to handle in a ServiceWorker.

The install event fires when a ServiceWorker is first fetched. Here is your chance to prime the ServiceWorker cache with the fundamental resources that should be available even while users are offline

event fires when a ServiceWorker is first fetched. Here is your chance to prime the ServiceWorker cache with the fundamental resources that should be available even while users are offline The fetch event fires whenever a request originates from your ServiceWorker scope, and you’ll get a chance to intercept the request and respond immediately, without going to the network

event fires whenever a request originates from your ServiceWorker scope, and you’ll get a chance to intercept the request and respond immediately, without going to the network The activate event fires after a successful installation. You can use it to phase out older versions of the worker – we’ll look at a basic example where we deleted stale cache entries

Let’s go over each event and look at examples of how they could be handled.

Installing Your ServiceWorker

A version number is useful when updating the worker logic, allowing you to remove outdated cache entries during the activation step, as we’ll see a bit later. We’ll use the following version number as a prefix when creating cache stores.

var version = 'v1::' ;

You can use addEventListener to register an event handler for the install event. Using event.waitUntil blocks the installation process on the provided p promise. If the promise is rejected because, for instance, one of the resources failed to be downloaded, the service worker won’t be installed. Here, you can leverage the promise returned from opening a cache with caches.open(name) and then mapping that into cache.addAll(resources) , which downloads and stores responses for the provided resources.

self.addEventListener( "install" , function (event) { console .log( 'WORKER: install event in progress.' ); event.waitUntil( caches .open(version + 'fundamentals' ) .then( function (cache) { return cache.addAll([ '/' , '/css/global.css' , '/js/global.js' ]); }) .then( function () { console .log( 'WORKER: install completed' ); }) ); });

Once the install step succeeds, the activate event fires. This helps us phase out an older ServiceWorker, and we’ll look at it later. For now, let’s focus on the fetch event, which is a bit more interesting.

Intercepting Fetch Requests

The fetch event fires whenever a page controlled by this service worker requests a resource. This isn’t limited to fetch or even XMLHttpRequest . Instead, it comprehends even the request for the HTML page on first load, as well as JS and CSS resources, fonts, any images, etc. Note also that requests made against other origins will also be caught by the fetch handler of the ServiceWorker. For instance, requests made against i.imgur.com – the CDN for a popular image hosting site – would also be caught by our service worker as long as the request originated on one of the clients (e.g browser tabs) controlled by the worker.

Just like install , we can block the fetch event by passing a promise to event.respondWith(p) , and when the promise fulfills the worker will respond with that instead of the default action of going to the network. We can use caches.match to look for cached responses, and return those responses instead of going to the network.

As described in the comments, here we’re using an “eventually fresh” caching pattern where we return whatever is stored on the cache but always try to fetch a resource again from the network regardless, to keep the cache updated. If the response we served to the user is stale, they’ll get a fresh response the next time they request the resource. If the network request fails, it’ll try to recover by attempting to serve a hardcoded Response .

self.addEventListener( "fetch" , function (event) { console .log( 'WORKER: fetch event in progress.' ); if (event.request.method !== 'GET' ) { console .log( 'WORKER: fetch event ignored.' , event.request.method, event.request.url); return ; } event.respondWith( caches .match(event.request) .then( function (cached) { var networked = fetch(event.request) .then(fetchedFromNetwork, unableToResolve) .catch(unableToResolve); console .log( 'WORKER: fetch event' , cached ? '(cached)' : '(network)' , event.request.url); return cached || networked; function fetchedFromNetwork (response) { var cacheCopy = response.clone(); console .log( 'WORKER: fetch response from network.' , event.request.url); caches .open(version + 'pages' ) .then( function add (cache) { cache.put(event.request, cacheCopy); }) .then( function () { console .log( 'WORKER: fetch response stored in cache.' , event.request.url); }); return response; } function unableToResolve () { console .log( 'WORKER: fetch request failed in both cache and network.' ); return new Response( '<h1>Service Unavailable</h1>' , { status: 503 , statusText: 'Service Unavailable' , headers: new Headers({ 'Content-Type' : 'text/html' }) }); } }) ); });

There’s several more strategies, some of which I discuss in an article I wrote about ServiceWorker strategies.

As promised, let’s look at the code you can use to phase out older versions of your ServiceWorker script.

Phasing Out Older ServiceWorker Versions

The activate event fires after a service worker has been successfully installed. It is most useful when phasing out an older version of a service worker, as at this point you know that the new worker was installed correctly. In this example, we delete old caches that don’t match the version for the worker we just finished installing.

self.addEventListener( "activate" , function (event) { console .log( 'WORKER: activate event in progress.' ); event.waitUntil( caches .keys() .then( function (keys) { return Promise.all( keys .filter( function (key) { return !key.startsWith(version); }) .map( function (key) { return caches.delete(key); }) ); }) .then( function () { console .log( 'WORKER: activate completed.' ); }) ); });

You should look at the full code on the GitHub repository!