Service Workers - Practical Guided Introduction (several examples)

In this post, we are going to do a practical guided Tour of Service Workers, by focusing on one of its most important use cases: Application Download and Installation (including application versioning).

As a learning exercise, I invite you to code along, and turn your application into a PWA by making it downloadable and installable! We will be doing the same to a sample application, available in this repository.

If you have tried to learn Service Workers before, you might have noticed that many of the features of Service Workers and the Service Worker Lifecycle can, at first sight, seem a bit surprising.

Why would we need a separate daemon instance to intercept the HTTP requests of our own application, where we can't really do long-running calculations or access the DOM?

And yet Service workers are the cornerstone of a Progressive Web App, they are the key component that binds all other PWA APIs together and enable the support of native-like capabilities such as:

  • Offline Support
  • Application Download, Installation, and Versioning
  • Background Sync
  • Notifications
  • Physical device interaction (Web Bluetooth)
  • Payments (via the Payment Request API)

Are PWAs really taking off?

With all these native-like capabilities, PWAs are here to stay! Here are some reasons why now is the best time to learn them:

What we will do in this post

Let's then start learning PWAs by example by doing an A to Z implementation of one of its key use cases: Application Download, Installation, and Application Version Management!

We will do this from first principles using directly the browser APIs, and show what is going on in each step using the Chrome PWA Dev Tools.

Note that we will build this Service Worker for learning purposes, as in production Service Workers are configured and automatically generated by build tools such as the Angular CLI or WorkBox.

Even by using these powerful tools, we will still need to know how Service Workers work under the hood, in order to be able to:

  • choose the right PWA tools
  • understand the scope of each tool
  • understand PWA tooling documentation
  • troubleshoot error scenarios
  • design a complete PWA solution

Table of Contents

In this post we will cover the following topics:

  • What is a Service Worker?
  • Application Download, Installation and Versioning in a Nutshell
  • Step 1 - Service Worker Registration
  • Step 2 - Service Worker Hello World Install Phase
    • The Cache Storage API
    • Background Application Download
    • The Service Worker Lifecycle (Consistency by Default)
  • Step 3 - Service Worker Activation Phase
  • Step 4 - Intercepting HTTP Requests
  • Step 5 - Purging Previous Application versions
  • Step 6 - Serving the Application From Cache Using a Cache Then Network Strategy
  • Customizing the Service Worker Lifecycle
    • Taking over the current page with clients.claim()
    • Skipping the Wait Phase (and potential issues it might cause)
    • Updating a Service Worker Manually
  • Built-in Browser protection against broken Service Workers
  • Precautions with the use of the Browser Cache and Service Workers
  • Conclusions

Let's then get started with our Service Worker Fundamentals deep dive!

What is a Service Worker?

A Service Worker is like background daemon process that sits between our web application and the network, intercepting all HTTP requests made by the application.

The Service Worker does not have access direct access to the DOM. Actually, the same Service Worker instance is shared across multiple tabs of the same application and can intercept the requests from all those tabs.

Note that for security reasons the Service Worker cannot see requests made by other web applications running in the same browser, and only works over HTTPS (except on localhost, for development purposes).

In summary: a Service Worker is a network proxy, running inside the browser itself!

Service Workers Overview

The code for the Service Worker is periodically downloaded from our website and there is a whole lifecycle management process in place.

Its the browser that at any time will decide if the Service Worker should be running, this is so to spare resources, especially on mobile.

So if we are not doing any HTTP requests for a while or not getting any notifications, it's possible that the browser will shut down the Service Worker.

If we do trigger an HTTP request that should be handled by the Service Worker, the browser will activate it again, in case it was not yet running. So seeing the Service Worker stopped in the Dev Tools does not necessarily mean that something is broken.

The Service Worker can intercept HTTP requests made by all the browser tabs that we have opened for a given domain and Url path (that path is called the Service Worker scope).

On the other hand, it cannot access the DOM of any of those browser tabs, but it can access browser APIs such as for example the Cache Storage API.

Service Worker Use Case: Application Download, Installation, and Versioning

You might be thinking at this point, what does network proxying have to do with application download and installation, and offline support?

The Service Worker is a network proxy with an installation lifecycle, but it's up to us to use it to implement native-like PWA capabilities: the Service Worker by itself does not provide those features.

So let's see how can we design a solution based on the Service Worker that will implement the background download and install use case.

Download and Installation Design Breakdown

Here is a summary of the design that we are about to implement:

  • we are going to download the Service Worker script from the server
  • we are going to make sure that the browser installs and activates the service worker in the background as late as possible in the application bootstrap time, in order not to disrupt the initial user experience
  • on the background, the service worker is going to download the whole web application (meaning the HTML, CSS and Javascript), version it and keep it for later
  • only the next time the user comes to the site, the service worker is going to kick in (more on this later)
  • this second time the user visits the site, the application will NOT be downloading the HTML, CSS and Javascript from the network - the Service Worker will serve the cached files that it had kept for later
  • This second time, the application startup will be much faster
  • The user will at least have a working application, even if the network is down

And this is how having a network proxy in the browser allows us to have installable web applications! This is all 100% compatible with the back and refresh buttons.

Let's then start implementing this design: first we need a sample application.

Step 1 - Service Worker Registration

Our starting point is a plain HTML, CSS and Javascript Bootstrap page, that used some very common CSS and Javascript bundles.

We will turn this simple page into a background downloadable and installable PWA, and the same design applies to a single page application: after all its just HTML, CSS and Javascript!

Reminder: The code for the sample application is available here in Github

The first step to turn this standard website into a downloadable PWA is to add a Service Worker via a registration script:

Notice the script sw-register.js, which is going to trigger the installation of our network proxy, the Service Worker. Let's then have a look at this registration script:

Let's break the registration process down line by line, and see what it means:

  • first, we are checking if the browser supports Service Workers, by looking for the serviceWorker property in the global navigator object
  • if the browser does not support SWs, then everything will still work, it's just that no installation will happen in the background, so we fallback to a normal web application scenario

When should a Service Worker be registered?

Even if we detect that the browser does support Service Workers, we are still not going to register a SW immediately! In this case, we are waiting for the page load event.

The load event is only triggered when the whole page is loaded, including its linked resources like images, CSS and Javascript and that can take a long time.

Why delay the registration of the Service Worker?

There are a couple of reasons why we want to delay the registration of the Service Worker: we want to avoid causing disruption of the initial user experience, as the application loads for the first time.

Browsers only do a limited amount of HTTP requests at the same time, and there is only so much network capacity. The Service Worker might or might not do separate network requests that can interfere with the ones needed to show initial content to the user.

This means that delaying the Service Worker registration prevents the Service Worker from degrading the initial user experience. Instead, the Service Worker will wait for the application to start up and then it will be installed in the background.

Note that in the case of a single page application, we might want to delay the registration even further, and wait beyond the load event.

The key is to understand that in the case of a Service Worker that does download and installation, we want to register it as late as possible, to avoid degrading user experience.

Service Workers and Consistency by Default

Another reason for delaying the registration of this type of Service Worker is to have consistent application behavior. Let's remember that the Service Worker will often serve the whole application itself!

So we want to avoid a situation where:

  • some of the page CSS and JS resources were served by the Service worker
  • while others came from the network

If some of the initial requests for a page came from the network, we want to make sure that all the remaining bundles were also loaded from the network as well, for consistency.

Avoiding inconsistent application scenarios

In the case of application download and installation, we want to avoid falling in a situation where we activate a Service Worker in the middle of a page startup.

This is because depending on timing conditions, we might accidentally fall into some hard to reproduce situation where the page is broken due to an unpredictable combination of HTML/CSS/JS artifacts, some coming from the network and the others from some sort of cache that the Service Worker is using.

In the next time that we visit this page the Service Worker will be active, and then we will load all resources from the Service Worker, instead of the network.

This means that again we will have a consistent set of bundles, all coming from a cache and corresponding to a given version of the application.

What happens at registration time?

In the example above, when the load event triggers we are going to call register() and identify the file sw.js as being a Service Worker script.

The browser is then going to download the sw.js file, and version it by creating a snapshot of all the bytes contained in this file. In the future, even if one single digit changes, the browser will consider that there is a completely new version of the Service Worker.

What is the service worker scope property?

The scope property determines what set of HTTP requests can be intercepted by the Service Worker, or not. In this case, the scope is '/', meaning that our Service Worker will be able to intercept all HTTP requests made by this application.

If the scope would instead be /api, then the Service worker would not be able to intercept a request like for example /bundles/app.css, but it would still be able to intercept a REST API request such as /api/courses.

Multiple Service Workers in the same page? Service Worker ID

This means that its possible have multiple Service Workers running on the same page, but on different scopes!

If a Service Worker would have a unique identifier, it would be the combination of the origin domain plus the scope path.

And this how the browser determines if two different scripts correspond to two different versions of the same Service Worker (and not based on keeping the same SW sw.js file name).

If two Service Worker scripts have the same scope path and even a byte of difference, the browser is going to consider them two versions of the same SW and install the latest version in the background.

Can I place the Service Worker in any folder?

The location of the sw.js file is important: if this file would be placed in a folder /service-worker/sw.js, then it would not be able to intercept requests like /bundles/app.css or /api/courses.

Instead, the maximum scope of HTTP requests that the Service Worker could intercept, would be any requests starting with /service-worker, the folder where the script is on!

Given this, we could, for example, register different service workers for different scopes: one service worker for all /bundles requests and another for all /api requests.

As we can see, there is a ton of flexibility! Right now, for implementing Download and Installation, we are going to use the root / scope and use only one Service Worker.

Step 2 - Service Worker Hello World

When the browser identifies a new version of the Service Worker for a given scope, it will trigger the install phase, which results in the emission of the install Lifecycle event.

Note that the Service Worker spec does not define what happens exactly in the install phase. That is up to us to implement that, by listening to the install event in sw.js.

After installation comes activation, and then network interception is ready to be used! Let's understand exactly how the installation and activation phases work, based on this Hello World Service Worker sw.js example:

An HTTP Logging Interceptor

This code is actually the implementation of a simple logging HTTP interceptor, and we will evolve it to implement Application Download & Installation!

Right now, let's break down this initial Hello World example, and see what is going on here:

  • we are using a reference to self: this means the current global context where the code runs, which would for example be the window if this would run at the level of the application
  • However, in this case, self points to the Service Worker global context
  • we are subscribing to the install and activate events, and logging their occurrence to the console
  • each logging statement is prepended with the version of the Service Worker, this will help us understand how multiple versions work
  • the install and activate steps both pass a Promise to waitUntil(), right now this is just to show how we would do async operations in these phases
  • if the promise passed to waitUntil() resolves successfully, then the installation/activation phase is completed successfully
  • if on the other hand, the promise is rejected then the installation/activation phase fails, and the next phase won't be triggered
  • we have also subscribed to the fetch event. Using it, we are intercepting all the HTTP requests made by the application
  • The fetch event has a method called respondWith(), which takes as argument also a promise
  • The promise we pass it needs to return (when resolved) the response to the HTTP request

Async operations in the install and activate phases

As we can see, like almost all PWA-related APIs, the Service Worker API for these lifecycle phases is Promise-based. During these phases, we can do asynchronous operations like for example fetch resources from the network.

In order to mark a phase as completed, we return a Promise that when resolved will successfully mark the phase as completed. In this case, both the install and activate phases return a Promise that gets successfully resolved, and so the application is now ready to start intercepting network calls.

Using the fetch event to intercept HTTP requests

Let's now have a closer look at the callback of the fetch event, which contains the HTTP logging functionality.

As we can see, this fetch callback is going to return the actual response of the HTTP call using respondWith(), and the response can be calculated asynchronously by passing a Promise to respondWith().

Note: the application code will be unaware of where this response came from: if from the network or from the Service Worker

We can take the response passed to respondWith() from anywhere, for example:

  • we can forward the call to the network and send back the network response
  • or we can retrieve the response from Cache Storage
  • we can even build a Response() object manually

In this case, here is what we are doing:

  • we are logging the URL of the intercepted request
  • then we forward the HTTP request to the network using the Fetch API
  • fetch() will return a Promise, that if resolved will deliver the network response, or fail in case of a fatal network error
  • note that fetch() will only throw an error if the network is down or some other fatal condition occurs like a DNS error. For example, an HTTP status code of 500 Internal Server Error would not cause the fetch promise to error
  • then we pass the fetch() promise that will emit the network response to respondWith()

Viewing the Hello World Service Worker in action

This response passed to respondWith() is then going to be passed to the application! As we can see, this Service Worker acts as a logging proxy.

From the point of view of the application, this response served by the Service Worker is indistinguishable from a call made if the Service Worker was not present, the only side effect is the logging in the console.

Let's then examine the console output:

v1 INSTALLING 
v1 INSTALLED
v1 ACTIVATING
v1 ACTIVATED
Service Worker registration completed ...

And here is our Service Worker running in the Chrome Dev Tools (Application Tab):

Service Worker v1

While coding along with this post, it's better to leave the "Update On Reload" option set to off, in order to better understand the Service Worker Lifecycle

This initial logging example is everything that we will need to understand in detail the Service Worker Lifecycle.

Why isn't the Service Worker immediately active?

You might have noticed one thing: although we are logging the installation and activation events, but there was no HTTP request logged to the console which means that fetch event does not seem to be working!

It's like the fetch logging interceptor is not working, even though the Service Worker is active.

But if we open another tab, or refresh the same tab, here is what we have:

v1 HTTP call intercepted - getbootstrap.com/dist/css/bootstrap.min.css
v1 HTTP call intercepted - localhost:8080/carousel.css
v1 HTTP call intercepted - code.jquery.com/jquery-3.2.1.slim.min.js
v1 HTTP call intercepted - getbootstrap.com/js/vendor/popper.min.js
v1 HTTP call intercepted - getbootstrap.com/dist/js/bootstrap.min.js
 ... other intercepted CSS/Js bundles
v1 HTTP call intercepted - localhost:8080/sw-register.js
Service Worker registration completed ...

So it looks like the Service Worker started intercepting HTTP requests only after we reloaded the page. That's a bit surprising the first time we see it, but this happens by default to ensure consistency.

The Service Worker Lifecycle, and Consistency by Default

The Service worker behavior we see here, although surprising at first it's actually a great feature that is very well thought out.

In all these scenarios: initial page load and Service Worker activation, opening a new tab or refreshing the original tab, there is something going on that is common to all scenarios:

Either all the HTTP requests of the page were served by the Service Worker, or none at all! This is what happened here:

  • the first time we loaded the page, none of the requests were served by the Service Worker
  • but when the first refresh occurred, or we opened a new tab, all of the requests were served by the Service Worker

And this ensures consistency: one version of the page, one version of the service worker. This avoids a whole class of some very hard to troubleshoot error scenarios.

How do Service workers interact with Browser tabs?

Let's now simulate some normal user behavior. What happens if we open other browser tabs of the same application?

v1 HTTP call intercepted - getbootstrap.com/dist/css/bootstrap.min.css
 ... the same HTTP requests, all served by version 1
Service Worker registration completed ...

We are going to see that this page is being served by the exact same SW Version 1! Note that the console logging is shared across tabs, which can be rather surprising.

If you refresh the application a couple of times and then switch back to another tab you are going to see logged HTTP requests that were made in the other tab.

This is actually expected, because we have the same Service Worker intercepting the requests from all tabs.

Service Worker Versioning In Action

To further understand the Service worker Lifecycle, let's now see what happens if we modify something in the Service Worker code. Let's, for example, modify the version number to v2.

Notice that we don't need to change the name of the file sw.js to notify the browser that a new version of the Service worker is available.

The browser will see that both versions are linked to the scope /, and if there is even one character of difference between both versions, the browser will install the new version.

Let's then try to install v2, still with multiple tabs opened. If we change the version number of the SW script to v2 and open another tab, here is what we see in the Dev Tools:

Service Worker v1

As we can see, the new version of the Service Worker is not immediately applied, it's in some sort of Waiting state!

And if we look at the console, we now have:

v1 HTTP call intercepted - getbootstrap.com/dist/css/bootstrap.min.css
v1 HTTP call intercepted - localhost:8080/carousel.css
 ... the same requests as before still being intercepted by v1
Service Worker registration completed ...
v2 INSTALLING 
v2 INSTALLED

There are a couple of things that are very interesting in this log:

  • version v1 was not installed again, or even activated
  • it looks like version v1 remained active during the whole refresh process, because it kept intercepting HTTP requests
  • all requests are still being intercepted by v1
  • Version v2 was Installed in the background, but not Activated!
  • Version v2 is now in the Waiting state

A couple of important questions come to mind here:

Why is the new version v2 Installed but not Activated?

One reason is that we have multiple tabs opened, and we want to show to the user a consistent experience. It would be confusing for the user to have two tabs opened running different versions of the same application.

And because Service workers intercept and modify HTTP requests, two different versions of the service worker might mean two different versions of the application itself!

So how will the browser handle this new version of the Service worker running on the / scope?

The browser is going to go ahead and perform any Installation operations like download bundles or an offline page in the install phase of v2, but the browser will not Activate v2 as long as there are multiple tabs opened still running v1.

This consistency by default is a key design goal of the Service Worker Lifecycle!

Now, before continuing to explore the Lifecycle, a quick note about browser Hard Refresh and Service Workers.

Service Workers and Hard Refresh

If something is unclear while trying out Service Workers, trying to do a hard refresh (Ctrl+Shift+R) will not help in the learning process.

This is because if you hit hard-refresh, the whole Service Worker is going to be bypassed, and it won't control the page - This is the standard browser behavior which is unlikely to change.

Ctrl+Shift+R is meant to bypass all network caches, and because the Service Worker is often used for caching, it bypasses it too.

With this important note out of the way, let's continue to dig deeper into how the Service Workers Lifecycle works, and how it enables Application Download and Installation.

Let's understand why at this stage with v2 already Installed why is v1 still running, and why v2 is not yet Active.

Why even with only one tab opened the new SW version will not become active?

We did refresh our single tab running v1, but still, v2 was not activated: v2 was Installed in the background, but not Activated.

This is because, from the point of view of the browser, the current page remains active until the refresh completes, and only then the page gets swapped out when we have at least received the response headers from the server.

And because the page was kept during part of the refresh process, the only way to ensure consistency is to keep it active all the way through the whole process.

After that, because we have kept the Service worker v1 active during the refresh, we want by default to keep it running after the refresh completed as well, which explains why V1 is still active after the page refresh is completed.

How to activate the new Service Worker version V2 then?

One way would be to use the skipWaiting option in the DevTools, but let's not do that! Let's instead reproduce the normal user experience: let's close all tabs running service worker v1, and open a new tab.

If we look at the console output, we now have:

v2 ACTIVATING
v2 ACTIVATED
v2 HTTP call intercepted - localhost:8080
v2 HTTP call intercepted - getbootstrap.com/dist/css/bootstrap.min.css
... the same list of requests, all intercepted by v2

As we can see, this time the browser activated Service Worker v2 that it had previously installed in the background, and v2 intercepted all the network requests from this page, meaning V2 is now Active!

And with this, we have now a good understanding of the Service Worker lifecycle so let's summarize.

Service Worker Lifecycle Summary

We can see that although a bit tricky at first sight, the way that Service Worker Lifecycle works makes a lot of sense. The Lifecycle is all about:

  • showing only one version of the application to the user
  • not disrupting the user experience
  • not delaying application startup
  • by default, avoiding version mismatches between the page and the Service Worker

This last point is especially important for the Download & Installation Use Case that we are about to review.

Let's remember, one of the common use cases of Service Workers is to cache the whole application, meaning literally all the HTML, CSS and Javascript!

Where does the Service Worker store those files then?

The Cache Storage API

At installation time, the Service Worker is going to fetch from the network all the bundles that together make a given application version, and then it's going to store them in a browser cache know as Cache Storage.

Like the Service Worker API, Cache Storage is also Promise-based and very easy to use. Let's then take this API and use it to implement the installation phase of the Download and Install use case.

Step 1 - Implementing Background Application Download

Let's then start adapting our Hello world logging interceptor example, and extend it with background Installation capabilities.

The first thing that we are going to do is, we are going to download all the Javascript and CSS Files in the background during the Install phase, and we are going to add those files directly to Cache Storage:

Again a lot is going on here in this example, so let's break it down step-by-step:

  • the first thing that we are doing is, we are getting a reference to an open cache, using caches.open() which returns a Promise
  • we are appending a version number to the cache name, meaning that as new versions are released, new caches will be created
  • Then we are doing a series of HTTP requests to fetch all the files that make a given version of the application
  • We are then adding all those files directly to cache storage
  • the key of the cache is the Request object used to make the HTTP request
  • the values stored in the cache are the HTTP Response objects themselves, that we can serve straight to the application
  • the addAll() call returns a Promise, that will resolve successfully if all the HTTP requests made to load each file work

Inspecting the contents of Cache Storage

In our case, the download of all the files worked, meaning the Install phase ended successfully! So let's now see what we have stored in Cache Storage, using the Chrome Dev Tools:

Service Worker v3

This panel is available on the same Application tab in the Dev Tools, under the collapsible menu named Cache Storage.

Note: If you open the menu and cannot find the new cache content, then right-click on the Cache Storage node and click Refresh

As we can see, all the application bundles have been downloaded in the background, and the application is ready to be served from the cache!

But before doing so, let's go ahead and first clear all previous versions of the application from Cache Storage.

Step 2 - Purging Previous Application versions

The best moment to purge previous versions of the application is at Service Worker Activation time, because this is the only moment that we can be sure that the user is no longer using the previous application version in any of the browser tabs.

This is how we can purge previous application versions at Activation time:

As we can see, we are looping through all the cache names available in Cache Storage, and deleting all caches that don't correspond to the current application version (which is V3).

A note on the async/await syntax

Notice that caches.keys() is returning a Promise, like in general it happens with Cache Storage API calls.

We want to wait for that Promise to resolve and then use that value in the rest of the code below, and so we are applying the await syntax that will wait for the Promise to resolve before continuing.

As we can see, this is a great way to make asynchronous Promise-based code to look much more readable and closer to synchronous code, but this only works inside a method annotated with the async keyword.

This async/await syntax is already available in a lot of browsers (see here for support), for example in Chrome you can try these examples without any transpilation needed.

Step 3 - Serving the Application From Cache With a Cache Then Network Strategy

The last step needed for implementing Application Download and Installation is to serve the application bundles from Cache Storage directly, and fallback to the network if necessary:

Let's then break down this example, to see how the Cache Then Network strategy is being applied:

  • we are intercepting all HTTP calls made by the application, inside an async function
  • the async function will always return a Promise to respondWith(), either explicitly as a return value, or by transparently wrapping the returned value in a Promise
  • inside the async function, we start by opening the cache that corresponds to the current application version
  • we are then going to query the cache, to see if there is an HTTP Response that would match the HTTP Request made by the application
  • the call to match() also returns a Promise, so we will await for the result before continuing
  • if a match was found, this means that the request made by the application was found in the cache, so we return that HTTP Response straight to respondWith()
  • note that there is no need to return a Promise from the async method, if we return a value it will implicitly get wrapped in a Promise by the async/await mechanism
  • if no match was found, we are going to let the request go through to the network by awaiting for the result of a fetch() call
  • then we are going to log the request that got forwarded to the network, and return the result of the fetch() call to the application

With this in place, any request that the application makes to load the cached bundles will be served from Cache Storage, while other requests such as for example a REST API call to /api/courses will still go through to the network.

And with this last step in place, we have a complete solution for downloading and background installing our web application! So let's try this out.

Deploying a new Version of the application

To see the Download and Install mechanism in action, let's open a new tab in our sample application, and see that it's now running version V3 of the Service Worker, which implements the Download and Install feature.

Note: here is the complete version v3 of the Service Worker

And here is the current console output:

v3 Serving From Cache: bootstrap.min.css
v3 Serving From Cache: carousel.css
v3 Serving From Cache: jquery-3.2.1.slim.min.js
v3 Serving From Cache: popper.min.js
v3 Serving From Cache: bootstrap.min.js
...

As we can see, all the CSS and Javascript bundles are coming directly from Cache Storage and not from the network, as expected. What would happen now if we would have a new version of the application?

Imagine that we did a ton of modifications to the application, like changing its design or applied a new theme.

How is the user going to get that new version v4, if version v3 is still being served each time directly from the cache?

In order to trigger the installation of version V4, the first thing that we need to do is to do also a small change to the Service Worker, for example incrementing the version number:

Let's now close all tabs except one, and refresh the browser. Here is what we would have on the console:

v3 Serving From Cache: bootstrap.min.css
v3 Serving From Cache: carousel.css
....
v4 Service Worker installation started 

As we can see, V3 of the Service Worker (and of the application) are still up and running, as expected. This means that the application version was served by Service Worker v3, which means the bundles all came from the Cache named app-cache-v3.

But we can see also that version V4 was Installed in the background. Let's have a look at what we have on the Service Worker tab:

Service Worker v4

As we can see, version V4 is waiting to be Activated. But the bundles of V4, which could correspond to a completely different version of the whole web application are now ready to be used.

To confirm this, let's have a look at the contents of Cache Storage:

Service Worker v5

As we can see, Cache Storage contains two versions of the application at this stage:

  • version v3, which is still being served to the user
  • version v4, which was downloaded in the background and is ready to be used as soon as all version v3 tabs are closed

In order to activate version v4, let's simulate some normal user interaction. The user would eventually close all the browser tabs running version v3, and then come back later to the application.

At that moment, the browser will activate version V4 and serve the corresponding files from the cache:

v4 Service Worker activated
v4 Serving From Cache: bootstrap.min.css
v4 Serving From Cache: carousel.css
....

And with this, the whole lifecycle is completed and the user now has a freshly updated version of the application downloaded and installed in the browser.

The new version of the application was donwloaded and installed in the background, without interfering with the normal user experience. This is actually even better then native mobile installations!

Customizing the Service Worker Lifecycle Behavior

What we have described so far was the default behavior of the Service Worker Lifecycle, which makes a lot of sense in the context of the Donwload and installation use case.

Let's now see how can we customize the Lifecycle if needed, to better suit other PWA use cases.

Notice that modifying the behavior of the Service Worker Lifecycle although tempting, is not really recommended, as we will see.

Skipping the Wait Phase (and potential issues it might cause)

For example, we could skip the Waiting Phase altogether of the Service worker Lifecycle, by calling the skipWaiting() API at the end of the Install phase:

In this example, we are awaiting for the files to be downloaded and installed, and then we are going to call self.skipWaiting(), which will return a Promise.

This will cause the Waiting Phase of the Lifecycle to be skipped, and for the new version of the Service Worker to become immediately active.

This means that if the user opens a new tab, the new version would be active which might lead to inter-tab inconsistencies. In most cases, it's better to not skip the Waiting phase and avoid those inconsistent scenarios by design.

This does not mean, however, that by using skipWaiting() the new version of the Service Worker can immediately intercept requests from the running tab.

Taking over the current page with clients.claim()

We have seen that for example that the very first time that a page with a Service Worker is loaded, the Service Worker will be Installed and Activated, but it will somehow still not be able to intercept the network requests made by the page.

We would have to refresh the page in order have the new Service Worker to start intercepting requests.

Again, this is for consistency: if the initial requests of a page were not served by a Service Worker, then by default none of the HTTP requests made by that page after startup will be served by the Service Worker either.

But we can change this, by having the Service Worker claim all the active application tabs at Activation time:

Calling claim() will allow the Activated Service Worker to immediately start intercepting requests (Ajax included) from the running page (as well as other open tabs), without having to wait for a reload.

This early activation of the Service Worker brings the potential for an inconsistency: we might end up with a page served by version v4 to have its runtime HTTP requests intercepted by Service Worker v5.

But for some use cases, this early activation is what we need: imagine a second service worker running on scope /api that caches application data on IndexedDB: we might want to activate it as soon as possible, to cache the application data as soon as possible.

Updating a Service Worker Manually

By default, the browser will check upon user navigation if there is a new version of the Service Worker on the server ready to be installed.

If by some reason, we have an application that is going to remain opened for a long period of time (like a PWA installed to the user Home screen), we can manually check if there is a new version of the Service Worker by using the registration object like this:

If a new version of the Service worker is available on the server, the call to update() will trigger a new background installation.

This periodic check is usually not necessary, as the browser will already do this check very frequently with each user navigation, or with other events such as for example if a Push notification is received.

One good scenario when we would like to check if there is a new version is: what if the version that we are running has a bug? Let's then talk about what happens if something goes wrong with the application.

Built-in Browser protection against broken Service Workers

As you might imagine, caching the application on the user computer and bypassing the network is a bit dangerous: what if the version the user downloaded accidentally had an error?

There are a couple of built-in browser protections against this.

For example, the Service Worker will never intercept itself!

Meaning that the file sw.js that we pass to serviceWorker.register('sw.js') will never be intercepted by a fetch event.

However, this does not apply to the Service Worker registration script sw-register.js, so we need to make sure that we never cache that.

Service Workers and normal Browser caching

The standard browser cache mechanism based on the Cache-Control header is very easy to misuse, due to the confusing nature of its configuration options.

To avoid those issues, its recommended to get familiar with some common Caching Best Practices, as this will help with any application in general, not only with PWAs.

Errors made in setting up Cache-Control headers for our application will be troublesome in production even if we don't run a PWA, but the use of a Service Worker will make those problems much worse.

We might run into a situation where we have cached the Service Worker sw.js file in the standard browser cache, because it was served with a Cache-Control header that gives the file a long lifetime.

Let's say that the sw.js was served with a cache lifetime of one month:

Cache-Control: max-age=2592000

The browser will indeed cache the header, but because the file is a Service Worker it will cache it only for a maximum time of 24h instead of 1 month!

This is a great precaution, but still, the website would be broken for a full day before a patch can be installed. The simplest and safest solution is to never cache the Service worker file or its registration script.

Avoid caching the Service Worker file

This can be ensured by the server by marking these files explicitly as being immediately expired:

Cache-Control: max-age=0

And speaking of the normal browser cache, what about the caching headers for the CSS and JS bundles?

Precautions concerning the use of the Browser Cache and Service Workers

The CSS / Js bundles stored in Cache Storage will be loaded from the network, and those bundles could or could not be served with a Cache-Control header, meaning that potentially we have two caches in action, that might interfere with each other.

This could lead to trouble scenarios, like for example a new version of the Service Worker gets installed, but tries to load a new version of a JS bundle file, which did not change the file name!

But the file is cached in the normal browser cache, and the ancient version accidentally still gets served to the Service Worker.

This means that the installation of the Service worker completes successfully, but Cache Storage now has the wrong version of one of the bundles, meaning that the application installation is corrupted.

So how do we avoid running into these scenarios? The simplest is to apply the same caching policies that we would for a non-PWA application: different types of files need different caching strategies.

Cache-Control for CSS/JS bundles

For CSS and JS bundles, the simplest is to append to the file name a hash of the file content, or a version number, like for example: bootstrap.v4.min.css.

Then for these files, we can choose a very long max age, essentially declaring them immutable and caching them forever:

Cache-Control: max-age=31536000

If a new version of the file is available, the file name will change (this could be enforced by the build system) and the new version will be downloaded and cached.

This will avoid many common caching issues for both browsers that support Service Workers, and those who don't.

Loading Resource Bundles from Third Party Domains

In this example, we have downloaded bundles all from our local domain. But what if we would like to load CSS and JS bundles from other third-party domains from inside the Service worker, like for example from a CDN?

This is possible, but the third-party domain as to allow for that cross-origin request to be executed, just like any other CORS request.

This can be done by serving the bundle file with this header:

access-control-allow-origin: https://yourdomain.com

If we are serving these bundle files from a CDN like Amazon Cloudfront, and we want the files to be loadable via a cross origin request coming from any domain and not just https://yourdomain.com, we can instead use this header:

access-control-allow-origin: *

Conclusions

As we can see, all the multiple PWA features and the related PWA APIs make the most sense if we look at them together and in the context of a specific use case, instead of in isolation.

We can do much more than the download and installation use case that we covered, this was just an example that happens to be the best starting point to understand why the Service Worker Lifecycle was designed the way it was.

The core philosophy of the Service Worker spec is about putting these network proxying capabilities in the hands of developers, so that we can implement many different PWA use cases and patterns, as opposed to providing only a set of predefined offline patterns (like it was the case of Application Cache).

I hope that this post helps with getting started with Service Workers and that you enjoyed it! If you have some questions or comments please let me know in the comments below and I will get back to you.

To get notified when more posts like this come out, I invite you to subscribe to our newsletter:

Caching Best Practices

Service Worker Fundamentals

Video Lessons Available on YouTube

Have a look at the Angular University Youtube channel, we publish about 25% to a third of our video tutorials there, new videos are published all the time.

Subscribe to get new video tutorials:

Other posts on Angular

Have also a look also at other popular posts that you might find interesting: