Caching assets using Service Workers – Tutorial

- 8 mins

With more and more of our lives being conducted online, good connectivity to the internet has quickly become a requirement of our daily goings on. Consistent, let alone fast, connectivity is not guaranteed but there are somethings we can do as developers to make a user’s visit that bit snappier every visit. As an added bonus, we get to play with some of the newer JavaScript features including service workers, promises and fetch.

Progressive Enhancement is centered around the idea that every user should get a fully functional and accessible website, perfectly suited to their task. Any further enhancements are layered upon this base functionality if the device is capable of supporting the improvements. Service Workers (SW) are one such technology, becoming popular along with the ES2015 set of standards, allowing web sites to take advantage of persistent background processing and event hooks to enable web applications to work while offline.

Whilst it may be a lot of work to retrofit a website to work fully offline, adding a little service worker magic it not too difficult and can produce immediate results. In this quick tutorial I cover the code used to cache the CSS, JavaScript and logo of plymouthsoftware.com. Whilst only 53% of browsers currently support SW, those that don’t will simply ignore anything they do not understand without (hopefully) causing the end user any issues. When those browsers catch up, all the work will be done and the improvements will be automatic. The result is that users are saved from making three trips to the server for approx 100kb of data.

Whilst this saving is not astronomical, it has the potential to shave a full second off the total render time when on an average mobile connection. In addition, the server will receive fewer requests plus the groundwork for more exciting SW based features, such as push notifications and background syncing, will have been completed.

The site I am working on is a Jekyll static site using Gulp to help with preprocessing and optimising files. As such, both the CSS and JavaScript are concatenated and optimised as part of the deploy process, meaning I have only a couple of versioned files to cache. The build process you’re working with may be different, but the code to cache files will be largely the same.

Service worker lifecycle

Service Worker Setup

If this is you first time working with SW, I’d suggest you do some background reading on both it and Promises . A good understanding of both these APIs will save much head scratching. Links to relevant documentation and tutorials are at the end of this post. Its async nature means SW can be a little painful when debugging, especially in the early days. To help with this I recommend loads of console.logs() in you development code as well as keeping a close eye on the chrome tab available under chrome://inspect/#service-workers

With that said, let’s create the service worker file that will be installed on the end-user’s device. At the root of your apps directory structure add a file called serviceWorker.js, including the following code to test that everything is hooked up correctly.


self.addEventListener('install', function(event) {
    console.log(‘Service Worker Installing’);
});

As Service Workers are scoped, placing our file at the root of the project ensures it has access, and is accessible, to all files deeper in the directory structure. To include our SW in the website, open up any existing js file and within a document.ready (or similar) block, add this.


<span style="font-weight: 400;">if ('serviceWorker' in navigator) {</span>
<span style="font-weight: 400;">console.log('navigator available');</span>
<span style="font-weight: 400;">  navigator.serviceWorker.register('/serviceWorker.js').then(function(reg) {</span>
<span style="font-weight: 400;">    console.log('ServiceWorker registration successful with scope: ', reg.scope);</span>
<span style="font-weight: 400;">  }).catch(function(err) {</span>
<span style="font-weight: 400;">    console.log('ServiceWorker registration failed: ', err);</span>
<span style="font-weight: 400;">  });</span>
<span style="font-weight: 400;">}</span>

This checks to see if the device has the capability to run a SW. If available, it then registers the file we’ve just created before printing a message to the console if successful. Open up the project in your browser (over https) and check to see if the console messages are present. If they are, it’s time to get caching.

Head back over to the serviceWorker.js file and add the following variables to the top of the file.


<span style="font-weight: 400;">  'use strict';</span>
<span style="font-weight: 400;">  var CACHE_NAME = 'psw-cache-v1';</span>
<span style="font-weight: 400;">  var urlsToCache = [</span>
<span style="font-weight: 400;">    '/assets/site-logo.svg',</span>
<span style="font-weight: 400;">    '/css/main.css',</span>
<span style="font-weight: 400;">    '/javascript/application.js'</span>
<span style="font-weight: 400;">  ];</span>

Here were forcing the js engine to ‘use strict’ mode, which is part of the ES2015 standard. As this file does not get concatenated it’s safe to declare outside the scope of a function. Next is the name of the cache we’ll create and the files we’d like to add. As with any form of asset caching, file versioning is highly recommended but is outside the scope of this tutorial. For this project I’m using the gulp packages ‘rev’ and ‘rev:replace’ to version files at build time which updates this array of asset strings. I’ll leave it you to decide how best to version any cached assets.

Fetching from Cache

We now need to turn our attention back to the install event. Update your code to match the following.


<span style="font-weight: 400;">  self.addEventListener('install', function(event) {</span>
<span style="font-weight: 400;">    event.waitUntil(</span>
<span style="font-weight: 400;">      caches.open(CACHE_NAME).then(function(cache) {</span>
<span style="font-weight: 400;">        return cache.addAll(urlsToCache);</span>
<span style="font-weight: 400;">      })</span>
<span style="font-weight: 400;">    );</span>
<span style="font-weight: 400;">  });</span>

When the install event is triggered, the event is ‘held open’ until the internal Promise resolves, which in this case is the caching of files. Within the waitUntil() block we open the cache by name and call addAll() upon it, passing in our array of asset strings. With the assets cached locally, it’s time to serve them to our user.


<span style="font-weight: 400;">This is done via a fetch event listener.</span>
<span style="font-weight: 400;"> self.addEventListener('fetch', function(event) {</span>
<span style="font-weight: 400;">  </span> <span style="font-weight: 400;">// Intercept fetch request</span>
<span style="font-weight: 400;">    event.respondWith(</span>
<span style="font-weight: 400;">    </span> <span style="font-weight: 400;">// match and serve cached asset if it exists</span>
<span style="font-weight: 400;">      caches.match(event.request).then(function(response) {</span>
<span style="font-weight: 400;">        return response || fetch(event.request);</span>
<span style="font-weight: 400;">      })</span>
<span style="font-weight: 400;">    );</span>
<span style="font-weight: 400;">  });</span>

Here were intercepting a fetch request and checking to see if the resource is in our cache. We then return either the resource or the original request which will head off to the network. This cache first strategy is just one of many possible methods, which Jake Archibald has excellently outlined in his ‘offline cookbook’ blog post. If you’ve not come across the fetch API yet, check it out. It’s an excellent replacement for AJAX via XHR and will change your life…… possibly.

Our app is now serving cached assets to users, but what about when we update the code? How do we prevent stale CSS and JS from reaching the end user? Who you going to call? Cache busters!

 

Cache Busting

As part of the service worker life cycle we have the activate event, which will get called every time the SW is called into action. At the bottom of serviceWorker.js add the following:


<span style="line-height: 1.5;"> self.addEventListener('activate', function(event) {</span>
<span style="font-weight: 400;">    event.waitUntil(</span>
<span style="font-weight: 400;">    </span> <span style="font-weight: 400;">// Open our apps cache and delete any old cache items</span>
<span style="font-weight: 400;">    </span> <span style="font-weight: 400;">caches.open(CACHE_NAME).then(function(cacheNames){</span>
<span style="font-weight: 400;">    </span> <span style="font-weight: 400;">cacheNames.keys().then(function(cache){</span>
<span style="font-weight: 400;">    </span> <span style="font-weight: 400;">cache.forEach(function(element, index, array) {</span>
<span style="font-weight: 400;">    </span> <span style="font-weight: 400;">if (urlsToCache.indexOf(element) === -1){</span>
<span style="font-weight: 400;">    </span> <span style="font-weight: 400;">caches.delete(element);</span>
<span style="font-weight: 400;">    </span> <span style="font-weight: 400;">}</span>
<span style="font-weight: 400;">  </span> <span style="font-weight: 400;">    });</span>
<span style="font-weight: 400;">    </span> <span style="font-weight: 400;">});</span>
<span style="font-weight: 400;">    </span> <span style="font-weight: 400;">})</span>
<span style="font-weight: 400;">    );</span>
<span style="font-weight: 400;">  });</span>

Here we’ve opened our cache before cycling over the cache’s keys comparing them to the array of URLs we’re storing. If the cached item does not appear in the array it’s deleted, freeing up space on the user’s device and keeping us within our apps storage limit.

That’s it for our simple implementation of a service worker caching strategy. Hopefully this will get you up and running quickly with this exciting technology, with the added benefit of improving our users experience. Whilst there are many improvements to this code and strategy, it should give enough to give a jumping off point into the wider world of service worker loveliness. You can get hold of the full code as a gist.

What do you think of this method of client side caching? Are Service Workers the future. Let me know what you think by getting in touch on Twitter.

Further reading

Tony Edwards

Tony Edwards

Event Organiser and Software Developer

comments powered by Disqus
rss facebook twitter github youtube instagram linkedin medium stackoverflow tiktok