Skip to content

Instantly share code, notes, and snippets.

@oilsinwater
Last active February 13, 2018 09:49
Show Gist options
  • Save oilsinwater/d4a2677ab2fd3521bdcc2e952c5c9f59 to your computer and use it in GitHub Desktop.
Save oilsinwater/d4a2677ab2fd3521bdcc2e952c5c9f59 to your computer and use it in GitHub Desktop.
Caching files with the Service Worker

[TOC]

Caching files Service Workers

What is Cache API?

While this API was intended for service workers, it is actually exposed on the window so it can be accessed from anywhere in your script. The entry point is caches.

  • ** Used to store assets that have a URL
    • Formally: "a request to response map" (spec)
    • Methods
      • add(request), addAll(request), put(request, response), delete(request, options?)
      • keys(request?, options?), match(request?,options?), mathAll(request,options)
  • ** self.caches** : entry point, a collections of Cache objects

Building a simple application cache

Service Worker Event Action
install build cache; add initial resources
activate update cache
fetch retrieve from cache, network, or database

Dev responsibilities with Cache:

  • explicit update requests a service worker, handles updates to the cache. All updates to items in the cache must be explicitly requested.
  • cached items must be deleted Items will not expire and must be deleted. You are also responsible for periodically purging cache entries.
  • browser limits and deletes caches Each browser has a hard limit on the amount of cash storage that a given origin can use. The browser does its best to manage disk space, but it may delete the cache storage for an origin. The browser will generally delete all of the data for an origin or none of the data for an origin.
  • version all caches Make sure to version caches by name and use the caches only from the version of the script that they can safely operate on.

On install: caching the application shell

We can cache sites's static resources in the install event in the service worker. And we can cache the HTML, CSS, JavaScript, any static files that make up the application's shell, in the install event of the service worker.

**current service workers: ** It's important to note that while the install event is happening, any previous version of your service worker is still running and serving pages. So the things you do here mustn't disrupt that.

javascript
var cacheName = 'app-shell-cache-v1';
var filesToCache = ['/', '/index..html', ...]; 
self.addEventListener('install', function (event) {
  event.waitUntil(
     caches.open(cacheName)
     .then(functions (cache) {
       return cache.addAll(filesToCache);
       })
       .then(function () {
       return self.skipWaiting();
       }).
      );
    });

** Event.waitUntil ** takes a promise to define the length and success of the install. If the promise rejects, the installation is considered a failure, and this service worker will be abandoned. If an older version is running, it will be left intact. ** Caches.open and Cache.addAll ** returned promises. If any of the resources fail to fetch, the cache.addAll call rejects.

###On activate: remove outdated caches There are plenty of ways to do this, but in example below, we iterate over the list of keys from the cache, and then delete any caches that don't match the current cache name.

javascript
self.addEventLister('activate', function (event) {
  event.waitUntil(
      caches.keys().then(keyList => {
          return Promise.all(keyList.map(key => { 
            if (key !== cacheName) {
                return cache.delete(key);
                }
             }));
          }));
       return self.clients.claim();
       });

On fetch: retrieve with network fallback

If you're making your app offline first, the code example below is how you'll handle the majority of requests.

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

If the resource exists in the cache, this code will return it from there. Otherwise, it will send the request onto the network.

On network response to fetch

We can intercept the request in the service worker, cache a clone of the response, and send the response itself to the page. This approach works best for resources that frequently update, such as a user's inbox or article contents.

javascript
self.addEventListener('fetch', function (event) {
  event.respondWith(
      return fetch(event.request)
      .then(function(response) {
          cache.put(event.request, response.clone());
          return response;
        })
      );
   });

This is also useful for non-essential content such as avatars, but care is needed. If a request doesn't match anything in the cache, get it from the network, send it to the page, and add it to the cache at the same time. If you do this for a range of URLs, such as avatars, you'll need to be careful you don't bloat the storage of your origin. If the user needs to reclaim disk space, you don't want to be the prime candidate. Make sure you get rid of items in the cache you don't need anymore. Now, to allow for efficient memory usage, you can only read a response request's body once. In the code example above, .clone is used to create additional copies that can be read separately.

####On user interaction (1)

javascript document.querySelector('.article') .addEventListener('click', function (event) {

event.preventDefault();

var id = this.dataset.articleId;
})

When the user clicks on an element in our page, we can add the element to the cache. If the whole site can't be taken offline, you may allow the user to select the content they want available offline--for example, a video on something like YouTube, an article on Wikipedia, or a particular gallery on Flickr. Give the user a Read Later or Save For Offline button. When it's clicked, fetch what you need from the network and put it in the cache.

####On user interaction (2) The cache's API is available from pages as well as service workers, meaning you don't need to involve the service worker to add things to the cache. We create a cache with a name corresponding to the specific article, then we fetch the article and add it to the cache.

javascript
caches.open('mysite-article-' + id)
.then(function (cache) {
  fetch('/get-article-urls?id=' + id)
  .then(function (response) {
    return response.json();
    }).then(function (urls) {
        cache.addAll(urls);
        });
      });
    });

Approaches to serve files from cache

  • Cache falling back to network
  • network falling back to cache,
  • cache then network
  • generic fallback.

If you're making you're app offline first, this is how you'll handle a majority of requests. 1. The request is intercepted by the service worker. 2. We look for a match in the cache, and if that fails, 3. we send the request to the network. 4. We return the response.

Other patterns will be exceptions-based on the incoming request.

Cache falling back to the network

If the resource exists in the cache, this code will return it from there. Otherwise, it will send the request on to the network.

javascript
self.addEventListener('fetch', function (event) {
  event.respondWith(
      caches.match(event.request)
      .then(function (response) {
      return response || fetch(event.request);
      })
    );
  });
  1. The request is intercepted by the service worker.
  2. We send the request to the network, and if that fails, we look for a match in the cache.
  3. We return the response.

Network falling back to the cache

javascript
self.addEventListener('fetch', funcntion (event) {
  event.repondWith(
    fetch(event.request)
    .catch(function () {
    return caches.match(event.request);
    })
   );
 });

In the above code, we first send the request to the network using fetch, and only if it fails do we look for a response in the cache. This is a good approach for resources that update frequently, that are not part of the version of the site, for example, articles, avatars, social media timelines, game leaderboards, and so on.

Flaw : Handling network requests this way means the online users get the most up-to-date content, but offline users get an older cached version. if the user has an intermittent or slow connection, they'll have to wait for the network to fail before they get content from the cache. This can take an extremely long time and is a frustrating user experience.

Cache then network(1)

This is a better solution. Requests are sent from the page simultaneously to the code in the main JavaScript, not the service worker. This is a good approach for resources that update frequently, that are not part of the version of the site--for example avatars, social media timelines, and game leaderboards. Like we say, this approach will get content on screen as fast as possible, but still display up-to-date content once it arrives.

javascript
var networkDataReceived = false;
var networkUpdate = fetch('/data.json')
  .then(function(response) {
    return response.json();
    })
    .then(function (data) {
    networkDataReceived = true;
    updatedPage(data);
    });

This requires the page to make two requests, one to the cache and one to the network. The idea is to show the cached data first then update the page when and if the network data arrives. In the above code, we are sending a request to the network, and in the next part, the code is looking for the resource in the cache. The cache will most likely respond first and, if the network data has not already been received, we update the page with the data in the response. When the network responds, we update the page again with the latest information.

Cache then network(2)

javascript caches.match('/data.json') .then(function (response) { return response.json(); )} .then(function (data) { if (!netwokDataReceived) { updatePage(data); } }) .catch(function () { return networkUpdated; })

Now, sometimes you can just replace the current data when new data arrives. For example, the game leaderboard, again. But that can be disruptive with large pieces of content. This code looks for slash data.json in the cache. This will most likely respond before the request to the network and update the page if the network hasn't already responded.

  • Note:
    • If the network responds after the cache, it updates the page again.
    • If getting the response from the cache fails, it tries the network again as a last attempt.
    • If the request is not found in both the cache and on the network, respond with a pre cached, custom offline page.
    • If you fail to serve something from the cache and/or network, you may want to provide a generic fallback.

Generic fallback

javascript
self.addEventListener('fetch', function ('event') {
  event.respondWith(
      caches.match(event.request)
      .then(function (response) {
        return response || fetch(event.request);
        })
        .catch(function () {
        return caches.match('/offline.html');
        })
    );
 });

This technique is ideal for secondary imagery, such as avatars, failed post requests, you know, unavailable while offline pages, and so on. In practice, you'd have many different fallbacks depending on URL and headers. For example, a fallback silhouette image for avatars. The item you fall back to is likely to be an installed dependency, for example, cached on the install event of the service worker.

Remove outdated caches

javascript
self.addEventListener('activate, function (event) {
  event.waitUntil(
    caches.keys()
    .then(function (cacheNames) {
      return Promise.all(
        cacheNames.map(function (cacheName) {
          if (cacheName !== 'currentCacheVersion') {
            return caches.delete(cacheName);
            }
          })
         )
       })
    );
 });

Once a new service worker has been installed and a previous version isn't being used, the new one activates and you get an activate event. Now, because the old version is out of the way, it's a good time to delete unused caches. During activation, other events, such as fetch, are put into a queue, so a long activation could actually potentially block page loads. Keep your activation as lean as possible. Only use it for things you couldn't do while the old version was active. It's important to remember that caches are shared across the whole origin.

Resources

  • Offline Cookbook
  • Offline storage for Progressive Web Apps
  • Persistent Store
  • navigator.storage.estimate()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment