I listened to a Udacity course on offline first and it was sometimes fairly hard to follow the implementation of a Service Worker. Since then I became a lot more familiar with this topic and rest assured, offline-first is an awesome experience. And actually, it’s a lot simpler to implement the basic functionality, than I thought.

Getting started

To kick-start this tutorial, download this repository and check out the sw-start tag.

About the app

I wanted to make this application very simple, but still a working example. As you will see it doesn’t do much, just dynamically loads some content. Since I wanted to keep the focus on the ServiceWorker and caching, there is absolutely no optimization. For example I just load a big fat image, but in real life you would want to serve progressive and optimized images.

Layout

There are not many files, but for a quick overview:

  • index.html – this is the entry point, the application is loaded from this document
  • js/app.js – this is the main application, the contents are loaded by this script and it will register the ServiceWorker
  • sw.js – this file will contain the ServiceWorker implementation, note how it’s placed in the root folder (/) instead of the js folder, will tell more about it in a bit

Registering a ServiceWorker

Add this code to the beginning of the application, in this case, to the js/app.js file:

This code block registers a new ServiceWorker (SW from here on) if it is available in the browser and logs to the console if the registration was successful or if it failed. If you are not familiar with JavaScript promises, it’s time to quickly review them.

Why sw.js goes to /

First of all, it doesn’t have to. But. Within a few minutes we will intercept the fetch requests, basically every request which the browser sends while downloading resources for the page. The scope of the SW defines which requests it may work on/which contents does it control. For example if the scope would be /img/, the SW would only see the requests which are sent there.

And here comes an important limitation, the scope of the SW can not be higher in the hierarchy, than the location of the SW file. So if you put the sw.js file to the /js folder and try to register it with a scope ‘/’ like above, the registration will fail. If you put the sw.js file into the /js folder and register it with a scope of /js/, it will not see any requests outside this folder. The SW will see the requests on and below its scope, so if the scope is /, it will also see the requests going under /img/.

Note: the above scope definition is redundant, the default scope of the SW is where the SW file is located, which in this case is ‘/’ anyway

That’s it, ideally after reloading the page, if you open the Developer Tools, you will see on the console that the SW is registered. It doesn’t do anything at the moment, but soon it will.

Caching the application shell

It’s considered a good practice to cache the page skeleton or application shell right when the SW is installed. Let’s do this now. Add the following code to the sw.js file:

What you cache and what you don’t at this point, it’s somewhat up to you. For example I didn’t cache the picture now, because I’m pretending, that I don’t know if I will have to load it every time when the application is loaded. But generally speaking, this is a great place to instantly cache the files which are required to render the skeleton of your application.

About this code: self refers to the SW and when it gets installed it opens a cache, which is provided by the Cache API. Once it is open, it will fetch all the assets we defined in the cachedUrls array and store them in the freshly opened cache. The event.waitUntil will help keeping the event open until this potentially time consuming process is finished.

Dynamic cache

The application shell is now cached, but in the real life there will be resources which have to be cached dynamically. To do that, we have to catch the requests which download these resources. Listening to the fetch event is what we need, so let’s add this to the SW:

This is a bit longer piece, so let’s break down what happens:

  • the SW intercepts the fetch events within its scope
  • tries to match the request in the cache. There are several ways to match a request, in this case we use the request object to look up if such request was cached before
  • if we receive a response from the cache, return it
  • otherwise send a new fetch request now
  • if the fetch request returns with a status which indicates that we successfully fetched the resource, put it to the cache now (note, that while add and addAll fetch the resources, put expects an already fetched response, which we have now, so all good). Important: we cache the clone of the response. Once the body stream of a response (or request) were read, it is gone
  • even if the fetch returned with a not-ok status code and we don’t cache it, the SW returns the response to the requester
  • if there was an error during the fetch (it’s not a non-200 status code, but for example a network error because we are offline) we return a custom response. This might be some pre-cached (in the application shell above) “This page is temporarily unavailable” page, some placeholder image for pictures or whatever seems fit. You may even return different content based on the request

As you see, we always return something for the user. If the content is already cached, we return the cached content, otherwise we let them know, that we are having some trouble. You may also show some status message, that we are currently offline.

Main benefits

  • if the connection is interrupted, the customer doesn’t have to see the browser’s error message, you can fully customize what you show
  • if the connection slows down or the user is on lie-fi (customer is connected to a wi-fi, but actually the data will never be fetched, just keeps loading and loading and loading…), you can still show the application from the cache, plus whatever they visited before and let them know you are trying to fetch the data
  • these things are cached on the client side, so accessing them again is an instant performance boost

Of course you will have to handle obsolete cache entries in a real application.

Verify the results

If you open the Chrome Developer Tools, you should see on the Console, that the ServiceWorker was registered. Next check the Application tag, here you can:

  • see details of and reload the SW
  • clear storage (including the SW and the cache) to start a new test. Note, that if you make changes, the SW will still serve the old version from the cache
  • see the contents of the cache under the Cache Storage menu

If you make a fresh start and take a look at the cache storage, you will notice, that only the application shell is cached, the picture.jpg is not there yet. This is exactly why we cached the page skeleton when the SW was installed, so the application is available in the cache instantly. If you now, when the SW is already registered, reload the page, the SW will intercept the call to the picture.jpg and will cache it (you will see it was cached later, than the other resources). After the reload you will see the image added to the cache.

Screen shot showing the developer tools

Now go to the Network tab (or at the top of the page) and check the Offline checkbox. When it’s done, reload the page. Note, shift-reload (or other force reload) will still show the usual error, this is expected if the user bypasses the cache on purpose.

Screen shot showing the developer tools when offline

Yay! Happy? Happy. We were offline, but the page still loaded and it loaded pretty fast. Hope this little example helped you get started. If you want more and want to understand some hows and whys, check out The Offline Cookbook.




Tagged with