Implementing a Service Worker

I recently had the pleasure of attending FullStack 2016, an excellent three day conference hosted in London (just around the corner from where I work, which is handy). The event had five presentation streams being held at the concurrently, which guaranteed a few interesting talks for everyone. If you get a chance to attend in 2017, I highly recommend it.

The main theme I noticed running over the three days was a new development technique emerging called Progressive Web Apps (PWA). A Progressive Web App uses the latest browser technologies to bring a native look and feel to your web application. Imagine being able create a web application that performs like a native application and looks like a native application but is built on web technologies. Sounds too good to be true! No need to create multiple native applications, one for each OS; build one web application and then allow the user to “install” it so it looks and performs like it is native.

The features of a PWA include:

  • Add to home screen functionality
  • Push notifications
  • Responsive so flexes to any screen size
  • Background data synchronisation
  • Instant loading with the use of Service Workers
  • Secure loading through HTTPS (this is required for a service worker to function)

The final two bullet points touch on the key technology powering PWA’s: Service Workers.

What is a service worker?

A service worker is essentially a proxy that sits between the browser and the network. It allows us as developers to intercept network requests, manipulate and respond to them in a way that is advantageous to our application.

The big difference between a native application and a web application is that a native application is primarily based offline. If there’s no network, the experience may be very limited depending on the app developer choices, and you may not get the most up-to-date data, but you still get some sort of experience. However if you have no network connection with a web application…well you have no web application. You will most likely just get a blank error screen saying no network available. A very poor experience for the user.

This is where a service worker can help us, allowing us to create an offline experience by caching key assets that can still be accessed by the application. Once configured a service worker will step through to following stages:

  1. Register - Register a service worker with the browser, it doesn’t do anything yet but it is now available for configuration
  2. Install - On install the service worker can pre-cache any assets required for the offline experience
  3. Activate - With the service worker now active it can begin to monitor network activity and manage the cache state
  4. Fetch - The fetch event is fired every time a request is made. Here we can manipulate responses and hook into the pre-cache from the install event

Offline-first

This is where we as developers need to start thinking “offline-first”. Ask yourself, If your web application suddenly disconnected from the network, but you still had access to online assets, what functionality would be useful to the user? Some thoughts I’ve had around this are:

  • Latest x number of blog posts or press releases
  • Contact information and map to your office location
  • A simple game to keep the user occupied
  • A promotional, or just entertaining video (depending on size)
  • If the site is down, pull in a status feed from Twitter for example
  • A simple offline page mentioning they have connectivity issues

The list of creative uses for an offline page is endless, it really depends on your user demographic and the time you are willing to spend crafting them.

Think performance

Service workers don’t only improve a users offline experience, they can also improve their online experience too by way of performance. The fetch event gives a developer the power to monitor requests and craft custom responses back to the browser. What this allows us to do is look to see if a particular file is in the cache, and if it is serve it back from disk. If it isn’t in the cache it will request it from the network as per usual. Serving an asset from disk will always be quicker than a network request, so if you choose your assets wisely you can minimise the amount of new data being fetched on each page load. Just think of it as a browser cache that we have complete control over!

I’ve implemented a very simple service worker on this blog. If you open your browser developer console you will see a console info message stating that the service worker has been registered (assuming your browser supports them). To see it in action try disconnecting from your network and refresh the page. You should see a very basic offline page stating you have network connectivity issues. If you want to drill down into the inner workings of the service worker you will need Chrome Canary (version 54 at the time of writing). This version will give you a brand new application tab where you can view the active service worker and its cache.

Service worker is visible and active in Chrome Canary version 54.

Implementation

If you want to take a look at the full service worker JavaScript I’ve used it is available here. The script borrowed heavily from the following resources:

I’ve split the script down into the separate stages below with a brief set of comments for explanation.

Register

Add the following code to your HTML pages.

1
2
3
4
5
6
7
8
9
// check see if your browser supports service workers
if ('serviceWorker' in navigator) {
    navigator.serviceWorker
        // register the service worker script
        .register('/sw.js')
        // using promises tell us if successful or there was an error
        .then(reg => {console.info('Service Worker registration successful: ', reg)})
        .catch(err => {console.warn('Service Worker setup failed: ', err)});
}

Install

The code sections below sit inside the service worker JavaScript file (sw.js).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
self.addEventListener('install', (event) => {
    // extend the events lifetime until the promise resolves
    // SW won't be considered installed until all caching is complete
    event.waitUntil(
        // resolve the cache object that matches the name
        // it is created if it doesn't exist
        // return a Promise once resolved
        caches.open(cacheNameStatic)
            .then((cache) => {
                // add all the following resources to the cache
                return cache.addAll([
                    OFFLINE_URL,
                    FOUR_OH_FOUR_URL,
                    '/css/styles.css',
                    '/css/styles.min.css',
                    '/images/bubble.svg',
                    '/images/cloud-r.svg',
                    '/images/fox-bg.png',
                    '/images/grid.png',
                    '/images/iceburg.svg',
                    '/images/lines.png',
                    '/images/narwhals-bg.jpg',
                    '/images/whale.svg'
                ]);
            })
    );
});

Activate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
self.addEventListener('activate', (event) => {
    // extend the events lifetime until the promise resolves
    event.waitUntil(
        // return a Promise that resolves to an array of cache names
        caches.keys()
            .then((cacheNames) => {
                // passes an array of values from all the promises in the iterable object
                return Promise.all(
                    // map over the cacheNames array
                    cacheNames.map((cacheName) => {
                        // if any existing caches don't match the current used cache, delete them
                        if (currentCacheNames.indexOf(cacheName) === -1) {
                            return caches.delete(cacheName);
                        }
                    })
                );
            })
    );
});

Fetch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// NOTE: the fetch event is triggered for every request on the page. So for every individual CSS, JS and image file.
self.addEventListener('fetch', (event) => {
    // only respond if navigating for a HTML page
    // see https://googlechrome.github.io/samples/service-worker/custom-offline-page/ for more details
    if (event.request.mode === 'navigate' ||
        (event.request.method === 'GET' && event.request.headers.get('accept').includes('text/html'))) {
        event.respondWith(
            // make sure the request we are making isn't in the cache
            fetch(createCacheBustedRequest(event.request.url))
                .then((response) =>{
                    // if the response has a 404 code, serve the 404 page
                    if(response.status === 404){
                        return caches.match(FOUR_OH_FOUR_URL);
                    } else {
                        // check see if the response is in the cache, if not fetch it from the network
                        return caches.match(event.request)
                            .then((response) => response || fetch(event.request));
                    }
                })
                // If catch is triggered fetch has thrown an exception meaning the server is most likely unreachable
                .catch((error) => caches.match(OFFLINE_URL))
        );
    } else {
        // respond to all the other fetch events
        event.respondWith(
            caches.match(event.request)
            // if the request is in the cache, send back the cached response. If not fetch from the network
                .then((response) => response || fetch(event.request))
        );
    }
});

Learning Resources

There are some great resources around the web if you want to learn more about service workers, I have listed a few below (remember to check out the two links above too):

I hope this post gave you a little insight into the potential of the service worker, and the Offline-first era we are about to enter into! If you know of any others resources, get in touch and I will add them to the list.