Service Worker Lifecycle Explained

Service Worker Lifecycle Explained Thumbnail

The Service Worker lifecycle is arguably the most complex part around Service Workers and Progressive Web Apps (PWAs). I only felt like I understood it completely while writing this article, and I hope it will make your life with Service Workers a lot easier.

In this article, I'll explain the different states that the lifecycle has and what you need to know about them. Afterward, we'll see how we can update Service Workers in a fast and easy way (using skipWaiting and clients.claim).

Table of Contents

A Short Introduction to Service Workers

Service Workers provide the core-features of Progressive Web Apps. They are similar to Web Workers but extend their capabilities by being able to intercept network requests or delivering push messages. Just like Web Workers, they also run on a separate thread from the browser.

For this tutorial you can think of a Service Worker like a proxy between the network and the browser. If you want a more detailed introduction to Service Workers, you can check out this article and come back later.

Lifecycle Overview

Service Worker Lifecycle Diagram

The states of the Service Worker are:

  • Download and parse
  • Installing
  • Installed/Waiting
  • Activate
  • Activated
  • Redundant

In my mental model, I separate transitional states from regular states. Transitional states transition between two regular states (like Installing and Activate) of the lifecycle and can be accessed by adding a listener to the Service Worker code (e.g. self.addEventListener('install', handler).

Regular states can be accessed through the registration object like this:

navigator.serviceWorker.ready.then((registration) => {
  console.log(registration.waiting); // "waiting" or any other state
});

We'll see how to do this for each state in the next steps.

Download and parse

Before we even start with the first step of the lifecycle illustration, the browser needs to download and parse the code of our service worker. This happens when we call the .register() function in our application:

navigator.serviceWorker
  .register('/service-worker.js')
  .then((registration) => {
    // some code
  });

If there are no errors, the Service Worker moves on to the first state of the lifecycle.

Installing

We can listen to this within the Service Worker by adding a event listener on the install event:

self.addEventListener('install', event => {
  // some code
});

At this point, the Service Worker is not yet controlling the page. It's a good moment to cache the files we want our Service Worker to serve once activated. To do so, we need to use the function event.waitUntil(promise).

This will tell the Service Worker to not continue in the lifecycle until the promise we passed to the function has successfully resolved.

self.addEventListener('install', event => {
  const preCache = async () => {
    const cache = await caches.open('static-v1');
    return cache.addAll([
      '/',
      '/about/',
      '/static/styles.css'
    ]);
  };
  event.waitUntil(preCache());
});

Example from the MDN docs. Caching files in this state is called precaching.

If anything goes wrong and the promise gets rejected, the Service Worker will become redundant and will never control the page.

Installed/Waiting

Once installed, the Service Worker will skip the waiting step and become active if there is no other Service Worker currently controlling the page.

However, if there is, it will keep waiting until all tabs and windows of this origin have been closed.

We can skip this state by calling self.skipWaiting and self.clients.claim(). I'll explain what they do and how to use them later on.

We can access the Service Worker in the waiting state from our browser context through the registration object like this:

navigator.serviceWorker.ready.then((registration) => {
  console.log(registration.waiting);
});

Activate

This a transitional state between Installed/Waiting and Activated. The Service Worker is now about to become active and control the current page.

During the transition it's recommended to clean up old caches:

self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.filter((cacheName) => {
          // Return true if you want to remove this cache,
          // but remember that caches are shared across
          // the whole origin
          return cacheName !== currentCacheName;
        }).map((cacheName) => {
          return caches.delete(cacheName);
        })
      );
    });
  );
});

Example adapted and slightly changed from the Google docs on caching with Service Workers. We delete all caches that don't match the current one (currentCacheName).

Activated

At this stage, the Service Worker is usually in control of the origin and serves the cached files. In this state, it will listen to the events fetch and message which allow us to serve requests from the cache and communicate with the Service Worker.

If the Service Worker is the first one to control the page, it is Activated but not controlling the page, meaning it doesn't intercept the fetch requests. We'll see how we can force it to become active with clients.claim in the next section.

We can access the Service Worker that is controlling the page through the registration object:

navigator.serviceWorker.ready.then((registration) => {
  console.log(registration.active);});

Redundant

A Service Worker becomes redundant in the following cases:

  • Downloading or parsing the Service Worker failed
  • One of the transitional states failed (Installing, Activating).
    • The reasons for this could be that event.waitUntil(promise) got rejected or an error occurres in the execution of the JavaScript in the event handler.
  • It got replaced by a new Service Worker.

A Service Worker can be active in one tab and redundant in another if it got replaced by a new Service Worker that forcefully gained control over the page by calling self.skipWaiting.

Update the Service Worker faster

self.skipWaiting() and self.clients.claim() are both functions we can call in the scope of our Service Workers. They are often used in conjunction, however, they're not doing the same thing.

Before we jump into the specific functions we can use to update the Service Worker quicker, we have to keep in mind that the Service Worker of one origin controls all clients of that origin.

The origin is the domain of your website and a client can be a tab, window, or iframe that points to this domain.

Service Worker controlling multiple origins

Service Worker controlling multiple origins. In the Service Worker context, we can access the clients through self.clients.

self.skipWaiting

As I already mentioned before, this function allows us to skip the Installed/Waiting phase and the Service Worker will become active right away. It's not too important at what point in the lifecycle we call this function, as long as it is before the Service Worker reaches Installed/Waiting.

We can do so at the beginning of our Service Worker file, adding this line of code:

self.skipWaiting();

We can also see that a Service Worker is trying to become active in the Chrome DevTools under Application -> Service Worker.

Skip waiting in the Chrome DevTools

The orange point indicates that the Service Worker is trying to become active.

self.clients.claim

While calling skipWaiting makes the Service Worker become active, it is still not controlling all pages right away. Controlling a page means that the Service Worker listens to it's fetch events and, depending on your code, returns cached results.

This is quite confusing at the beginning, but let me illustrate this with an example app I wrote to understand this myself.

In opposite to skipWaiting, we need to call clients.claim after the Service Worker reached the Activate state.

self.addEventListener('activate', event => {
  event.waitUntil(clients.claim());
});

The example app is a simple HTML site that registers a Service Worker. I set up a few buttons that interact with the Service Worker to be able to play around with it more easily. The Service Worker file has a variable at the top of the document that increments on every GET call, which will make the browser think there's a new version available. You can check out the source code here, I already apologize for the code quality in advance.

First visit example app

On the very first visit, the Service Worker becomes active immediately but it doesn't intercept any fetch requests yet. The Service Worker will start controlling the page in one of these cases:

  • The user navigates away from the page (to another origin) and comes back again.
  • The user opens the page in another tab or window.
  • The user refreshes the page once.

We can test this behavior in my example app as well:

  1. Open the page in incognito.
  2. Open the same page in another tab.
  3. Go back to the first tab and check the Service Worker DevTools

Client claim exaplained

We can see that a new section โ€” Clients โ€” appears in the Service Worker DevTools, which tells us that the Service Worker is now controlling our page. If we click on the URL it will lead us to the other tab.

If we go back to the first tab and hit the clients.claim() button, we can see two clients appearing in the DevTools now.

Service Worker active but not controlling

Service Worker active but not controlling the page


Service Worker controlling

Service Worker controlling the page. Can be forced by calling clients.claim().

After trying this out myself, I found that calling clients.claim affects on the first-page visit. After that, calling skipWaiting is sufficient for the next Service Worker to become active.

Conclusion

The Service Worker lifecycle was quite hard to understand for me, controlling different clients, the different states, and how to update Service Workers always was a headache.

I hope I could give you a better understanding of all those concepts and explained it in a way that made it easier to understand. If something is still unclear, please feel free to reach out to me and I'll incorporate your feedback into the article to help people even better with this topic.

If you want to transform your React app into a PWA, I recommend you check out my tutorial on this topic, walking you through all the necessary steps.

Thanks for reading.

Further reading