A 10-minute primer to Service Workers

A 10-minute primer to Service Workers
Photo by Carlos Irineu da Costa / Unsplash

In this article, we have a look at what service workers are, why they are used in modern web development and how you can add them to your web applications.

I just 10 minutes, you will learn:

  • what service workers are
  • which superpowers service workers provide you with
  • which security restrictions apply to service workers
  • why service workers can be useful to your web applications
  • how you can add a service worker to your web application

In the process, you will learn about the service worker lifecycle and specific service worker terms and concepts.

So let's get started!

What is a service worker?

A service worker is a script that an application can register with the browser when an application is loaded.

It is written in JavaScript, but in contrast to other JavaScript code in your application, the code from the service worker runs outside of the context of a web page.

As a result, a service worker is not tied to a specific web page. If you open 2 tabs in your browser and navigate one tab to https://your-site/page-one and another tab to https://your-site/page-two, they share the same service worker instance.

Service workers come with a few very unique superpowers:

  • A service worker can intercept fetch and push events that are sent by the browser.
  • A service worker has access to IndexedDB to store and retrieve data.
  • A service worker has access to a powerful cache API to store and retrieve network request/response pairs.
  • A service worker can communicate with web pages via the PostMessage interface.

With all superpowers comes great responsibility, so browsers have to make sure the service worker's superpowers cannot be used in a bad way. Without proper security measures, a developer with bad intentions could register a service worker that intercepts all network requests and steals the user's data.

To make sure a service worker cannot be used for malicious purposes, the browser imposes very strict security restrictions:

  • A service worker can only be registered on a page that is served over HTTPS. Localhost is an exception. You can register a service worker on localhost via plain HTTP so you don't need HTTPS during development.
  • A service worker does not have access to localStorage or sessionStorage.
  • A service worker is limited to its origin. For example, a service worked hosted on https://your-site/example/sw.js can only control clients under https://your-site/example/ while a service worker hosted on https://your-site/sw.js can control clients for the entire origin at https://your-site/.
  • A service worker can be stopped and started at any time by the browser so it cannot rely on keeping track of state within itself.

Now that you know what a service worker is, which superpowers it has and which restrictions it has to deal with, how can it be of benefit to your web application?

Why would you want to add a service worker to your application?

Service workers open a whole new array of exciting possibilities for modern web applications that were previously only available to native applications, such as:

  • push notifications: you can let your service worker opt-in to receive updates from a service like Firebase Cloud Messaging.
  • background sync: you can let your service worker manage outgoing network requests so the requests are queued when there is no connectivity and sent as soon as the connectivity is restored.
  • cache network requests: you can add offline support to your application by letting a service worker intercept network requests and cache the responses using the browser's cache API.

In short, a well-implemented service worker can make your application faster, more reliable and add offline support.

So how do you add a service worker to your application?

How can you add a service worker to your web application?

Before we have a look at how you can add a service worker to an Angular application, let's first have a look at how you can add a service worker to any web application in general.

In essence, adding a service worker to a web application involves 4 steps:

  1. Step 1: the registration of the service worker.
  2. Step 2: the installation of the service worker.
  3. Step 3: the waiting phase of the service worker.
  4. Step 4: the activation of the service worker.

Let's have a deeper look at each of the steps.

Step 1 - the registration of the service worker.

The registration step involves telling the browser where it can download the service worker code. In the example below, we tell the browser that the service worker code can be download from a file called sw.js in the root of our project:

// index.html

// Make sure service worker is supported
if ('serviceWorker' in navigator) {

  // Wait until window has loaded to make sure registration
  // of service worker does not interrupt page load.
  window.addEventListener('load', registerServiceWorker);
}

function registerServiceWorker() {
  navigator.serviceWorker
    .register('/sw.js')
    .then(
      function (registration) {
        // Registration was successful
        console.log('ServiceWorker registration successful')
        console.log('ServiceWorker scope: ', registration.scope);
      },
      function (error) {
        // Registration failed
        console.log('ServiceWorker registration failed: ', error);
      }
    );
}

We tell the browser to register the service worker that is hosted on /sw.js as soon as the web page has finished loading.

As a result, the browser downloads the service worker code from /sw.js. If the download fails, the service worker is never activated. If the download succeeds, the service worker is ready for installation.

Step 2 - the installation of the service worker.

Once the service code has been downloaded, the browser executes the code from /sw.js.

You can hook into the installation process by listening to the install event inside sw.js:

// sw.js

self.addEventListener('install', function(event) {
  console.log('The service worker is installed');
});

The install event handler is a perfect place to precache files. Precaching files means downloading files and storing them in a cache so that the precached files can be read from the cache when the service worker is activated.

As a result, the application can be loaded much more quickly because the service worker can serve the files directly from cache instead of having to download them via the network.

If you cache all files that are part of your application, the service worker can even serve your entire application when no internet connection is available, often termed as offline support. How cool is that!

Here is a short example from the Google Developers page on service workers:

// sw.js

var CACHE_NAME = 'my-site-cache-v1';
var urlsToCache = [
  '/',
  '/styles/main.css',
  '/script/main.js'
];

self.addEventListener('install', function(event) {

  // Precache files
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});

If the service worker fails to execute the installation logic, it is never activated.

If the service worker succeeds to execute the installation logic, it enters a waiting state until it can activate.

Step 3 - the waiting phase of the service worker.

By default, a service worker is only activated when a zero client state is reached. We already learned that a service worker is tied to a scope, which is determined by the service worker's URL:

  • a service worker at https://your-site.com/sw.js can control all clients under https://your-site.com/ and its subdirectories
  • a service worker in https://your-site.com/example/sw.js can control all clients under the https://your-site.com/example/ and its subdirectories

A client can be a web page, a worker or a shared worker.

During the waiting phase, the browser waits until all clients are closed that fall under the service worker's scope, including the page that registered the service worker.

To illustrate, suppose that you navigate your browser to https://your-site.com, a service worker is registered at https://your-site.com/sw.js and that you have the following tabs open in our browser:

  • Tab 1: https://your-site.com/
  • Tab 2: https://your-site.com/news/
  • Tab 3: https://google.com

Here, the service worker remains in a waiting state until both tab 1 and tab 2 are closed because they are both clients that fall under the service worker's scope https://your-site.com/ and can thus be controlled by the service worker.

If you close only tab 1, and open a new tab with https://your-site.com/index.html, then the service worker remains in a waiting state, because tab 2 is still open.

If you close both tab 1 and tab 2 and open a new tab with https://your-site.com/index.html, then the service worker is activated and takes control over the page in the new tab because a zero client state was reached before the new tab was opened.

Step 4 - the activation of the service worker.

When a service worker is activated, it can control clients. And when a service worker controls a client, the service worker can intercept the client's fetch and push events.

To intercept fetch and push events, the service worker can listen to the corresponding events.

In the following example from the Google Developers service worker primer, outgoing network requests are cached and cached responses are returned when available:

// sw.js

// Listen to activate event to execute logic when service
// worker is activated
self.addEventListener('activate', function(event) {

  // Perfect place to clean up caches from older service worker versions
  console.log('The service worker is activated');  
});

// Listen to fetch event to cache outgoing HTTP requests
self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        if (response) {
          return response;
        }
        return fetch(event.request);
      }
    )
  );
});

We already covered a lot, so let's recap what we learned so far.

Summary

In this article, we learned:

  • what service workers are
  • which superpowers service workers provide us with
  • which security restrictions apply to service workers
  • why service workers can be useful to our applications
  • how we can add a service worker to our application

We learned about the service worker lifecycle that involves 4 steps:

  1. step 1: the registration of the service worker
  2. step 2: the installation of the service worker
  3. step 3: the waiting phase of the service worker
  4. step 4: the activation of the service worker

And in the process, we learned about a few important concepts:

  • A service worker has a scope, which is determined by its URL.
  • During the service worker's waiting phase, the browser waits until a zero client state is reached before the service worker is activated.
  • When a service worker is activated, it can control clients.
  • A client can be a web page, a worker or a shared worker.
  • When a service worker controls a client, the service worker can intercept the client's fetch and push events

In the follow-up article, we have an in-depth look at 2 special service worker concepts: skipWaiting() and clients.claim(), which impact they have on the service worker lifecycle and how using them improperly can lead to errors and even data loss.

So stay tuned for more service worker awesomeness!

If you wish to stay informed, please feel free to leave your email address below and I'll send you a short note whenever an existing article is updated or when a new article is available.