Hi, I am Sanjeet Tiwari...
Let's talk about

Back to Notes

Service Workers

Web JavaScript

A service worker is a JS script which gets registered with the browser which can run even when the user is offline. It stays registered and can load content as well when offline.

They can be used for -

When a service worker is registered with the browser, it can intercept the request and can decide what happens with that request. It sits between Client and remote server.

A service worker functions like a proxy server, allowing you to modify requests and responses replacing them with items from its own cache.

Service Worker as an Interceptor

Limitations of Service Workers

Service Worker Lifecycle and Events

There are majorly 3 steps in the lifecycle of a service worker -

Service Worker Lifecycle

Registration

The way we can register any service worker is by using register method of navigator.serviceWorker API.

if ('serviceWorker' in navigator) {
    navigator.serviceWorker
        .register('./<path_to_sw_file>.js', {
            scope: "/" // default value
        })
        .then(reg => console.log('Service Worker registered'))
        .catch(err => console.error('Service Worker not registered'))
}

Installation

This is the step wherein we specify what assets we need to cache and we open up a Cache Storage via CacheStorage API.

// in the service worker file

// a name for the cache, which will appear under 
// 'Cache Storage' section in Dev Tools
const cacheName = 'v1';

// An array of path names of the files we want to cache
const cacheAssets = [
    '...',
    '/../..',
];

self.addEventListener('install', (e) => {
    console.log('Service Worker Activated);

    // now we need to tell the browser to wait until
    // assets have been cached
    e.waitUntil(
        caches
            .open(cacheName)
            .then(cache => {
                console.log('Caching Assets');
                cache.addAll(cacheAssets);
            })
            .then(() => self.skipWaiting())
    )
})

Doing this step alone does not mean that the cached content will be available offline. We need to set up the cache for offline viewing.

Activation

Once the version of the service worker that you specified with cacheName has been activated, you may want to delete or invalidate the old caches that may be there in Cache Storage.

The primary use of activate is to clean up resources used in previous versions of the service worker.

// Removing old caches
self.addEventListener('activate', (e) => {
    console.log('Service Worker Activated');

    // wait until all the cache keys are traversed
    // and the ones whose name is !== cacheName are
    // cleared
    e.waitUntil(
        // loop through all cache keys
        caches.keys().then(cacheNames => {
            // loop through the array and return a
            // Promise which deletes the cache
            return Promise.all(() => {
                cacheNames.map(c => {
                    if (c !== cacheName) {
                        // delete this cache
                        console.log('Deleting old cache', c);
                        return caches.delete(c);
                    }
                })
            })
        })
    )
})

A special note

Whenever there is an update to the service worker, and the new service worker is installed, it doesn’t straight away gets activated. It waits for the open pages controlled by previous version of service worker to close first, and then the activate event of the new service worker is fired.

Offline Viewing

In order to view the cached assets even when offline, we need to attach one more event listener - fetch which listens to the requests made by the client.

Here, we need to manually return the cached response if the site is NOT live.

To do this, we will make a fetch request again, to see if it fails and then if it does, we will return asset from the cache.

self.addEventListener('fetch', (e) => {
    // we need to respond with a cached response
    e.respondWith(
        fetch(e.request).catch(_err => {
            console.log('Will fetch from cache');
            return caches.match(e.request);
        })
    )
})

Now, cached assets will be available offline.

Cache the entire site

One more way of caching assets of a site is to cache the entire response received by the browser.

In this scenario, cache is not opened in the install event, but it happens now in the fetch event.

self.addEventListener('fetch', (e) => {
    e.respondWith(
        // return a fetch call
        fetch(e.request)
            .then(res => {
                // we need to make a clone of the response
                // we can do this by clone method
                const resClone = res.clone();
                caches
                    .open('v2')
                    .then(cache => {
                        // we need to this copy of the response
                        // against the request
                        cache.put(e.request, resClone);
                    })
                return res;
            })
            .catch(err => {
                // if fetch fails
                return caches.match(e.request)
            })
    )
})

This way the entire response for a fetch request can be cached.

The navigation preload feature of service workers enables workers to start downloading resources as soon as fetch requests are made, and not waiting for the service worker to be activated.

The way we can activate it is via -

self.addEventListener('activate', (e) => {
    e.waitUntil(self.registration?.navigationPreload.enable());
})

Then use event.preloadResponse to wait for the preloaded resource to finish downloading in the fetch event handler.

The delay addressed here may not be significant in most of the cases, but are a possibility.

An example timeline

Service Worker Timeline

Sources

Intro To Service Workers & Caching

Intro To Service Workers & Caching

Traversy Media

Sanjeet's LinkedIn

Using Service Workers

MDN

Last updated on 30-07-2024