PWAs - Introduction to Service Worker and Caching Strategies

SegunSegun
6 min read

The web has evolved over the years and so have the applications built to run on it. Web apps have evolved because there are better ways to build them unlike in the past. Thanks to PWAs, these days we have apps built to deliver a better user experience than ever before

In this article, we’ll focus on helping you understand PWAs, how service workers form a part of the core for a PWA and the various caching strategies that birthed PWAs.

What are PWAs

PWAs, short for progressive web apps, are web apps, except that, unlike typical web apps, they are built with modern technologies to give an improved user experience similar to native apps.

Why PWAs

According to Zippia, there are about 6.65 billion smartphone users globally. This same report alluded that users spend about 5 hours daily. Interacting with apps on their smartphones. PWAs, unlike native apps, require less effort to build, download and install. They can work on almost all devices and screen sizes. And if a device doesn't support PWAs, PWA delivers progressively features to the target device and gracefully skips unsupported ones.

How do PWAs come about

Before the birth of PWAs, we used to have web applications, largely regarded as websites that could only run in a web browser and needed internet connectivity. PWA was and is still a game changer because, unlike traditional web apps, they can be installed on a device and work offline without internet connectivity. These two features are what sets a PWA apart from traditional web applications.

There are several technologies responsible for these features delivered by PWAs. One of such is the web app manifest that provides information used to instruct the browser on how the PWA should be rendered and displayed on a user's device. Another important technology needed for delivering a PWA is service workers.

Service workers

A Service worker is a type of web worker and they form the base of all PWAs. Like web workers, they are non-blocking and do not slow down the main browser thread. Although they are slightly different from web workers as they serve the purpose of intercepting network requests and implementing caching strategies for these network calls. In simpler terms, PWAs without service workers will be mere web apps because, without service workers, they will not work offline in the absence of internet connectivity.

In the following section more emphasis will be placed on caching strategies, but first, let's look at how to register and install a service worker for a PWA.

A typical code snippet to register and install a service worker would take this form:

// app.js
if ('serviceWorker' in navigator) {

    // Todo: Register the service worker here
    navigator.serviceWorker.register('/service-worker.js')
    .then((registration) => {
        console.log(registration)
    })
    .catch((err) => {
        console.log('err', err)
    })
}
// service-worker.js
const version = 1;
const cacheName = 'cache-' + version;
const offlinePageUrl = 'offline-page.html';
const urlsToCache = [
  'client.js',
  'style.css',
  offlinePageUrl
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(cacheName)
      .then((cache) => {
        return cache.addAll(urlsToCache);
      })
      .catch((err) => console.error('An error occurred', err))
  )
});

In addition to the above snippet, a service worker file will also contain snippets for caching strategies.

Caching Strategies

As much as we would like to implement caching, it is important to note that caching too many assets during the installation of a service worker might interrupt the whole installation if the service worker fails to fetch the resources. Below are some caching strategies we can take advantage of:

1. Cache only

This strategy is used to implement an offline-first approach to the app. Basically, the idea here is to cache static resources and fetch them when the service worker is being installed so they are available once the service worker is ready.

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((response) => {
        // The responce is in the cache
        if (response) {
          return response;
        }

          // If there is no cache match, the response will look
          // like a network error
      }
    )
  );
});

After a user first visit a PWA, those resources that were cached will be available to them on subsequent visits even if they did not have a valid network connection.

2. Network only

As the name implies, in this strategy we do not need to query the cache. The network-only strategy is ideal for logs or resources we do not need while offline. We would usually not need to implement this strategy because it is the browser's default behaviour.

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

3. Cache falling back to network

When a user interacts with a PWA for a second time, with this strategy if resources are available in the cache, this version is delivered. Otherwise, a network request will be triggered to fetch and then cached.

In simpler terms; serve the user a cached version if available, or else trigger a network request. This strategy is adapted for resources that do not change too often, like their profile information for instance.

The whole goal is to deliver the fastest response.

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((response) => {
         return response || fetch(event.request);
      }
    )
  );
});

4. Network falling back to cache

This strategy is the opposite of the previous one. With this strategy, the goal is to deliver the latest data from the network request and only fall back to a cache version if the network request fails for any reason.

When the need is to display information that changes very frequently, this is the strategy to employ. Users without a valid network connection will be served old versions of the requested resource.

self.addEventListener('fetch', (event) => {
  event.respondWith(
    fetch(event.request)
      // If the network request fails...
      .catch(() => {

        // ..we attempt to get it from the cache
        return caches.match(event.request);
      }
    )
  );
});

5. Stale while revalidate

This strategy is quite similar to the cache-only strategy. The goal of this strategy is to ensure fast responses. To achieve this, data is served from the cache. While this is happening a separate network request is triggered to fetch newer versions of resources. If the request is successful, its result is cached.

Basically, the whole idea is to keep the cache up to date with up-to-date information.

self.addEventListener('fetch', function(event) {

    event.respondWith(async () => {
        const cache = await caches.open('cache-v1');
        const cachedResponse = await cache.match(event.request);
        const fetchPromise = fetch(event.request);

        event.waitUntil(async () => {
            const networkResponse = await fetchPromise;
            // Update the cache with a newer version
            await cache.put(request, networkResponse.clone());
        }());

        // The response contains cached data, if available
        return cachedResponse || networkResponse;
    }());
});

6. Cache and network race strategy

This strategy comes in handy when the goal is to provide the fastest possible response to the user. Here we attempt to get resources from both the network requests and cache at the same time and whichever one gets delivered first is presented to the client.


function raceRequests(promises) {
  return new Promise((resolve, reject) => { 
    promises.forEach(promise => 
      // We resolve immediately as one of the passed promises resolves
      promise.then(resolve)
    );

    promises.reduce((a, b) => a.catch(() => b))
      .catch(() => 
        // All passed promises have been rejected
        reject(Error("All promises failed")));
  });
};

self.addEventListener('fetch', function(event) {
  event.respondWith(
    raceRequests([
      // Attempt getting the resource from the cache
      caches.match(event.request),

      // Attempt getting the resource from the network
      fetch(event.request)
    ])
  );
});

Conclusion

PWAs are known to deliver better user experience and performance, thanks to caching strategies in service workers. These caching strategies form an important part of service workers in building a PWA. It is often possible to combine two or more caching strategies depending on the project requirement.

Thanks for reading this article till the end. If you have any questions or concerns, share them in the comment section.

0
Subscribe to my newsletter

Read articles from Segun directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Segun
Segun