Powering Up Your Web Apps With Service Workers

Jan Sommer
Level Up Coding
Published in
7 min readNov 4, 2020

--

Positano, Italy

An Overview

Today’s web applications are built upon a wide range of use cases, which are far from serving static documents.

We’re at a point where an application can be served as web based software with a feature set of a native app. These enhanced apps are called progressive web apps (PWAs).

An important aspect of a PWA is the service worker. A service worker is nothing less than a Javascript script, which runs in the background of the browser and acts as a proxy between the server and the browser. This gives your application the ability run without any network connection, thanks to caching, along with some other useful features like:

  • background synchronization
  • cache control
  • push notifications
  • network request interception

The purpose of a progressive web app is to give users the native-app experience with tools like service workers while it is served as a web application.

Progressive Web Apps (PWA) are built and enhanced with modern APIs to deliver enhanced capabilities, reliability, and installability while reaching anyone, anywhere, on any device with a single codebase.

— Sam Richard and Pete LePage on web.dev

What is a Service Worker?

A service worker is a Javascript worker which is running in parallel to your application. It acts as an additional layer between your web application and the server you’re sending requests to. This condition however causes some limitations which I also want to unroll in this section.

First, the service worker can’t access the DOM tree directly and therefore it can’t manipulate the web page and its HTML elements. You’re restricted to communicate with your web page via messages (see e.g. the MessageChannel API).

Furthermore a service worker operates fully asynchronous and relies heavily on promises. As a consequence, you can’t use synchronous APIs like thelocalStorage API.

When the service worker isn’t used it gets stopped by the browser to reduce resource usage. The service worker stays idle until it’s needed for any further operation. In case of an incoming request the browser will reactivate the service worker. In this process, any state, which was kept by the service worker, is lost. Therefore storing any state within the service worker is risky and should be avoided.

The web application can only have one active service worker at a time. But it is possible that two service workers are installed. It’s, for example, the case when a new version of the service worker is waiting for the old one to get terminated. This could cause a misconception: the previous (still running) worker is serving cached assets, but you’re expecting that the updated service worker version is already serving and thus providing the latest assets. Nevertheless, there are ways to prevent this behaviour, which will be covered in the Any Challenges? section.

Due to the fact that service workers are able to intercept and modify network requests, which could cause huge security flaws, they’re distributed only via HTTPS. But there’s one exception: localhost. So you can easily work with service workers on your local development environment.

In Chrome and in Firefox you can easily see and control the installed service worker by going to the Application tab.

Browser Compatibility

Service workers are supported by all modern browsers (Chrome, Firefox and Safari). See Can I Use… for a detailed overview.

You can explore the availability of the service worker API by checking the existence of the serviceWorker property:

if ('serviceWorker' in navigator) {
// service worker supported
}

Lifecycle

When working with a service worker, you have to be aware of its life cycle phases. These phases ensure, that you won’t stumble upon any conflicts with multiple versions of your service worker.

Registration

Before you can use a service worker along with your web app you have to register it. This is the main starting point for the whole life cycle. In this phase the browser will try to download and parse the service worker. A successful registration will trigger the next step: installation.

Installation

When the registered service worker does not match the already installed one (byte-wise compared), the browser will install it. You can use the installation phase for caching your static assets. After a successful installation it will get activated if it hasn’t been registered before or as soon as the previous service worker is terminated.

Active

Once the service worker is active, it’s able to handle requests, react to messages, run background synchronization and send notifications. You can also use this phase for clearing the current cache.

Idle

The service worker switches into the idle state when it is no longer handling requests and other tasks.

Terminated

When the service worker remains in the idle state for a specific time, the browser will terminate it to reduce resource consumption. This will give a newer version of the service worker the opportunity to take over control and be activated.

Any Challenges?

As we all know: with great power comes great responsibility. The rich and powerful feature set of the service worker API comes with some challenges we have to be aware of.

The biggest challenge is to manage different versions of a given service worker. Since a newly registered service worker isn’t directly replacing the current installed one. It’ll wait until the existing service worker stopped serving all of its clients.

This could cause an issue when you want to replace a broken service worker or when you want to change the files which it serves from the cache.

The first problem is: you can’t just create a new, updated service worker file and register/install it for your web app. Since the existing service worker will still serve the cached web page and not the one with the newly created service worker file, the browser won’t recognize the updated version and install it. Thus you always have to update the existing service worker file, which will lead to the next problem.

When you update the existing service worker, by adding new or fixed functionality or by changing the cached files, you’ll run into the problem that the updated service worker will wait for the current one to get terminated.

A solution could be the usage of self.skipWaiting(). This can be triggered within the installation phase and would force the updated service worker to get active and take over control. Keep in mind, that all these actions are running asynchronously and therefore could lead to the following situation: The old service already served the cached assets, but the new worker, which was activated by calling self.skipWaiting(), is running logic on new/updated assets.

All in all you have to be careful when updating an existing service worker. You should try to release versions which build upon each other or at least won’t cause any troubles when the previous version of your service worker is still serving the cached assets. If it’s applicable for your web app, you could run the refresh of the browser manually by calling the update() method on the service worker registration.

Getting Started

We’ll build a simple web page which is served and cached by a service worker. So let’s start with a simple HTML web page (index.html):

The web page contains only a header and a main element along with a script tag where we’ll now register our service worker:

At first we’re checking whether the given browser supports the service worker API. If it’s the case, we register the service worker script for a specific scope.

The scope declaration restricts the workers access to a certain part of your web application. In our example we’re configuring the service worker to handle requests, which are demanding resources located at the service workers current directory and in all directories beneath.

Next, we should add some caching functionality to our service worker. So let’s head to our service-worker.js file and add an event handler for the install event.

The event handler opens a cache with a specific, self-defined name and stores all desired files in it.

So once the service worker gets installed, all the files are stored in the cache. But right now these files won’t get served when they’re requested by the client. To resolve this, we have to listen to the fetch event. The event is dispatched whenever the client is requesting resources from a URL.

As soon as the fetch event arrives, the service worker will try to respond to it with

  • either the cached file (in case it can find it in the cache)
  • or it will fetch it from the URL

Let’s see the caching functionality in action by serving our web page and turning off the network connection:

Setting the network connection to ‘Offline’ in the Chrome DevTools

Refresh the website and a web page will still be served. When we check the network tab, we see that our web page is not downloaded but instead it is served by the service worker.

Chrome DevTools network tab

We’ve implemented a simple caching functionality for our web page by using the service worker API. The next steps could be implementing background synchronization, in case the client loses its connection and gets back online, or sending messages to the web page after running long-term tasks.

You can find the whole example app here:

--

--

Germany based software developer with a focus on web technologies