Basic intro into offline support with Service Workers

Joel Oliveira
Joel Oliveira
May 26 2023
Posted in Engineering & Technology

A quick look into offline support for web apps

Basic intro into offline support with Service Workers

In this post, we will explore how to get started with Service Workers, namely, how to develop a simple web app with offline functionality. Besides providing a way to cache assets to be served even when there isn't any network access available, Service Workers can also provide other functionality like Web Push notifications.

We will not cover all these features in this post, and simply focus on how to cache a web app's resources, intercept network request and serve those cached assets instead.

Before we start...

It's important to mention that Service Workers only work, for security reasons, when a page is served via HTTPS. You can however use localhost for development, as that is considered, by browsers, a secure origin.

If you are not familiar with Service Workers, this is a file hosted in your server that will run in the background even when your web app is not being used or a device is offline. It works like a proxy server. Once registered, installed and activated, it will control all the active web pages under its scope. For example, if a Service Worker is located at https://domain.com/directory/sw.js, it will control all pages in that directory. A page located at https://domain.com/about.html will not be controlled by that Service Worker.

Register a Service Worker

Let's jump into it. Let's assume your web app contains a index.html, and a directory called assets where you host all the images, stylesheets and javascript resources, in their respective folders.

├── assets
│   ├── css
│   │   ├── main.css
│   ├── images
│   │   ├── avatar.png
│   ├── js
│   │   ├── main.js
├── index.html
└── sw.js

Assuming that your index.html includes a reference to your main.js javascript file as follows:

<!DOCTYPE html>
<html>
<head>
    <title>My Web App</title>
</head>
<body>
  <script src="/assets/js/main.js"></script>
</body>
</html>

You can then register your Service Worker in main.js:

const registerWorker = async () => {
    if ('serviceWorker' in navigator) {
        try {
            const registration = await navigator.serviceWorker.register("/sw.js");
            if (registration.installing) {
              console.log("Installing");
            } else if (registration.waiting) {
              console.log("Installed");
            } else if (registration.active) {
              console.log("Active");
            }
        } catch (error) {
            console.error(`Registration failed with ${error}`);
        }
    }
}

registerWorker()

As you can see, this is pretty straight forward, you register the sw.js as your Service Worker, only for browsers that support it, this means that any other browsers will just use your web app when online.

Caching Assets

Now let's jump into your Service Worker. Once registered, the browser will attempt to install and activate it. You will then use the install event to populate the cache with the assets you need to run the web app. To cache these assets you will use the Service Worker's storage API (aka caches):

const version = 'v1'

const addResourcesToCache = async (resources) => {
    const cache = await caches.open(version);
    await cache.addAll(resources);
};

self.addEventListener('install', event => {
    event.waitUntil(
        addResourcesToCache([
            "/",
            "/index.html",
            "/assets/css/main.css",
            "/assets/js/main.js",
            "/assets/images/avatar.png"
        ])
    );
});

As you can see, you will use waitUntil to process your assets, which prevents the Service Worker to install until addResourcesToCache has been executed. This function will open the cache with a version number (this is important because a version number will allow you to update the Service Worker without disturbing the older version) and add all your resources. When everything is added, the Service Worker will be installed and activated.

Serve Cached Assets

Now that your web app's assets are cached, it is time to do something with it. Basically, you will use the fetch event, which is triggered every time a resource controlled by the Service Worker is requested. You will use it to intercept HTTP responses and serve your cached assets instead:

const putInCache = async (request, response) => {
    const cache = await caches.open(version);
    if (request.method !== 'GET') {
        // Can not cache non-GET requests
        return
    }
    await cache.put(request, response);
};

const cacheFirst = async (request) => {
    const responseFromCache = await caches.match(request);
    if (responseFromCache) {
        return responseFromCache;
    }
    const responseFromNetwork = await fetch(request);
    // Response stream can only be read once, so we need to clone it
    putInCache(request, responseFromNetwork.clone())
    return responseFromNetwork
};

self.addEventListener("fetch", (event) => {
    event.respondWith(cacheFirst(event.request));
});

Let's go through this very quickly. The cacheFirst function will try and match any requests to the assets you've previously cached. If it matches, it will serve the cached asset, if it doesn't, it will simply fetch that resource, put in the cache, and serve the network response instead. This allows you to add any new assets, your page might have added later to the cache, and serve those cached assets when requested again.

At this point, your web app is also accessible offline. You can try that by simply turning off your network connection and refresh your web app. The browser should still display it.

Updating a Service Worker

If your Service Worker was previously installed, and a new version is available, it will be installed in the background but can only be activated when all pages, still using the old version, are closed.

const version = 'v2'

const addResourcesToCache = async (resources) => {
    const cache = await caches.open(version);
    await cache.addAll(resources);
};

self.addEventListener('install', event => {
    event.waitUntil(
        addResourcesToCache([
            "/",
            "/index.html",
            "/assets/css/main.css",
            "/assets/js/main.js",
            "/assets/images/avatar.png",
            //...more assets
            "/assets/images/logo.png"
        ])
    );
});

In this example, you will update the Service Worker in the install event, for example, by adding new assets. To keep the previous version undisturbed, we will give the new Service Worker a different version number.

This makes it possible to create a new cache, even if there are pages being controlled by the old version of the Service Worker. As soon as those pages are closed, the Service Worker will be activated and serve the new cached resources.

Delete Cached Assets

Finally, you will want to delete old caches as soon as our new Service Worker is active. To do this, you will use the activate event, which is triggered when all pages, using the old version, are no longer being used and the new version starts working.

const deleteCache = async (key) => {
  await caches.delete(key);
};

const deleteOldCaches = async () => {
  const cacheKeepList = [version];
  const keyList = await caches.keys();
  const cachesToDelete = keyList.filter((key) => !cacheKeepList.includes(key));
  await Promise.all(cachesToDelete.map(deleteCache));
};

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

Once again, you will use waitUntil to block any other events until you are done removing old caches.

Ready to make it work?

Although this is a very basic introduction to offline web apps, it should be enough to get you started with Service Workers. If you are interested in learning more about it, you should take a look at this website.

As always, we hope you liked this article, and if you have anything to add, we are available via our Support Channel.

Keep up-to-date with the latest news