One of the major features of service workers is their ability to act as network proxies. Since they can intercept and handle requests from the pages they control, they give developers an incredible opportunity to customize how certain resources are handled. This includes control over how resources are cached, as well as over how these cached resources are utilized. This ability provides an opportunity for performance gain – especially when a resource is used on multiple pages on the site – as well as a chance to customize and enhance the offline experience. In today’s post, we’ll be looking at how to set up a basic cache using service workers. This includes getting a cache initialized and populated with an initial set of resources, as well as adding additional resources to the cache as the user browses the site.

Setting up the cache

To set up a cache, we’ll be using the caches.open() method. This method takes the name of the cache to open as an argument. We pass in this name, and it will either return the specified Cache object, or it will create one if it doesn’t yet exist. Once we have the cache we want, we can use cache.add() or cache.addAll() to add a url (or an array of urls) to the cache.

We can open a cache whenever we want, but one popular spot to do so is when the service worker is first ‘installed.’ This gives us a chance to prepopulate it with some important resources that we want to ensure are part of the cache from the very beginning. For example:

var CACHE_NAME = 'myCache';

// List of URLs we want to cache initially
var urlsToCache = [
  '/',
  '/assets/style.css',
  '/assets/site.js'
];

// Cache initial list of URLs when we first install the sw
self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(CACHE_NAME).then(function(cache) {
      return cache.addAll(urlsToCache);
    })
  );
});

Retrieving from the cache

Once we have resources in the cache, how do we retrieve them? To do this, we’ll set up a listener of the ‘fetch’ event.

Any time a resources is fetched, we’ll utilize the caches.match() method to see if the given request already has a cached response. If so, we can simply return that response instead of fetching the resource over the network. If it’s not cached (or we don’t want to serve the cached response), we can continue and return the non-cached response.

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).then(function(response) {
      // Returns the cached response (if it exists)
      if (response) {
          return response;
      }
      // Or continues original fetch
      return fetch(event.request);
    })
  );
});

Caching as you go

In addition to populating the cache on the installation of the service worker, we can also add to it at any time. This includes caching resources on an as-needed basis, as they’re retrieved from the network. For instance, if a user hits a non-cached page, we could store copies of whatever retrieve over the network into our cache in case the user returns to that page or needs those resources elsewhere. This allows us to keep the initial cache streamlined, and then grow it incrementally, based on the resources the user actually has needed. Here’s a simplified example:

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        // If already cached
        if (response) {
          return response;
        }

        // If not, fetch request, and then cache response
        return fetch(event.request).then(
          function(response) {
            if (!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }

            // Stash copy of response
            var cachedResponse = response.clone();
            caches.open(CACHE_NAME)
              .then(function(cache) {
                cache.put(event.request, cachedResponse);
              });

            return response;
          }
        );
      })
  );
});

Don’t forget to clone(): In this example, you’ll see that we use the clone() method when we stored the response in the cache. The response is a stream that can only be consumed once (i.e. by the browser or the cache), so if we want to be able to send it to both the browser and the cache, we have to clone() it so we can have a copy to work with.

Just a Start

This is just a basic cache, and there are many more way we could enhance it. In the next post, we’ll look at some ways we can optimize this even more, including setting up separate caches for different resource types, pruning them as needed, and setting up some offline fallbacks for both pages and images.