Service workers are a core part of Progressive Web Apps that allow caching of resources and web push notifications, among other things, to create an effective offline experience.

They act as a proxy between web applications, the browser, and the network, allowing developers to intercept and cache network requests and take appropriate action based on the availability of the network.

A service worker runs on a separate thread, so it is non-blocking. This also means that it does not have access to DOM and other APIs available in the main JavaScript thread like cookies, XHR, web storage APIs (local storage and session storage), etc. Since they are designed to be fully asynchronous, they heavily use promises to wait for responses to network requests.

Due to security concerns, service workers only run over HTTPS and cannot be used in private browsing mode. However, you do not need a secure connection while making local requests (good enough for testing).

Browser Support

The service worker is a relatively new API that is only supported by modern web browsers. Therefore, we first need to check if the API is supported by the browser:

if ('serviceWorker' in navigator) {
  // Supported 😍
} else {
  // Not supported 😥
}

Service Worker Registration

Before caching the resources or intercepting network requests, we must install a service worker in the browser. Since a service worker is essentially a JavaScript file, it can be registered by specifying the file path. The file must be accessible over the network and should only contain a service worker code.

You should wait until the page is loaded, then pass the service worker file path to the navigator.serviceWorker.register() method:

window.addEventListener('load', () => {
  if ('serviceWorker' in navigator) {
    // register service worker
    navigator.serviceWorker.register('/sw-worker.js').then(
      () => {
        console.log('SW registration successful 😍')
      },
      err => {
        console.error('SW registration failed 😠', err)
      }
    )
  } else {
    // Not supported 😥
  }
})

You can run the above code every time a page loads without any trouble; the browser will decide if the service worker is already installed or not and handle it accordingly.

Service Worker Lifecycle

The registration lifecycle consists of three steps:

  1. Download
  2. Install
  3. Activate

When a user visits your website for the first time, the service worker file is immediately downloaded, and installation is attempted. If the installation is successful, the service worker is activated. Any functionality inside the service worker file is unavailable until the user visits another page or refreshes the current page.

Browser Events

Once the service worker is installed and activated, it can start intercepting network requests and caching resources. This can be done by listening to events emitted by the browser inside the service worker file.

The browser emits the events listed below:

  • install is emitted when the service worker is being installed.
  • activate is sent when the service worker has been registered and installed successfully. This event can be used to remove outdated cache resources before installing a new version.
  • fetch is emitted whenever the web page requests a network resource. It can be anything: a new HTML document, an image, a JSON API, a stylesheet, or a JavaScript file, whatever is available in a remote location.
  • push is sent by the Push API when a new push notification is received. You can use this event to display notifications to the user.
  • sync is invoked when the browser detects network availability after the lost connection.

Serving Cached Resources

We can listen to the install event when the service worker is installing to cache specific resources that would be needed to serve the page when we are out of network:

sw-worker.js

const CACHE_NAME = 'site-name-cache'

self.addEventListener('install', event => {
  event.waitUntil(
    caches
      .open(CACHE_NAME)
      .then(cache =>
        cache.addAll([
          'favicon.ico', 
          'projects.json', 
          'style.css', 
          'index.js', 
          'https://fonts.googleapis.com/css?family=Open+Sans:400,700'
        ])
      )
  )
})

The above example code uses the Cache API to store the resources in a cache named site-name-cache.

The self is a read-only global property that is used by the service workers to get access to themselves.

Now let us listen for a fetch event to check if the requested resource was already stored in the cache and return it back if found:

sw-worker.js

// ...
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      if (response) {
        // found a cached resource
        return response
      }
      return fetch(event.request)
    })
  )
})

We look for a cache entry for the resource identified by the request property, and if not found, we make a fetch request to get it. If you want to cache new requests too, you can do it by handling the response of the fetch request and then adding it to the cache, like below:

sw-worker.js

//...
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      if (response) {
        // found a cached resource
        return response
      }

      // get the resource and add it to the cache
      return fetch(event.request).then(response => {
        // check if the response is valid
        if (!response.ok) {
          return response
        }

        // clone the response
        const newResponse = response.clone()

        // add it to cache
        caches.open(CACHE_NAME).then(cache => cache.put(event.request, newResponse))

        // return response
        return response
      })
    })
  )
})

Service Worker Update

When the service worker is installed, it continues to run until it is removed by the user or updated. To update a service worker, all you need to do is upload a new version of the service worker file on the server. When the user visits your site, the browser automatically detects the file changes (even just one byte is enough) and installs the new version.

Similar to the first-time installation, the new service worker functionality will only be available when the user navigates to another page or refresh the current page.

One thing we can do is listen to the activate event and remove the old cache resources. The following code does this by looping over all caches and deleting the cache that matches our cache name:

sw-worker.js

// ...
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(keys => {
      return Promise.all(
        keys.map(cache => {
          if (cache === CACHE_NAME) {
            return caches.delete(cache)
          }
        })
      )
    })
  )
})

That's all for the service workers' introduction. If you want to learn more, check out ServiceWorker Cookbook, a collection of working, practical examples of using service workers in modern websites.

✌️ Like this article? Follow me on Twitter and LinkedIn. You can also subscribe to RSS Feed.