Caching

Introduction

Modern browsers are smart. They will cache (save) information about a website (HTML, CSS, JavaScript, images, ...) and serve them from the cache the next time. This technique ensures that websites load faster and use less bandwidth. Keep in mind that as newer files are added to the cache, old ones might be purged to make space.

Service Worker caching (Cache API)

With Service Worker caching, you are in complete control and can even provide offline support.

Why support Offline Acces?

Still show information when a user:

  • has a poor connection
  • has no connection
  • has a LiFi connection

How?

  • store key (request) / value (response) pairs
  • Service Workers can access it
  • You can also access it from JavaScript in your pages
  • Instead of sending a Network request you can retrieve the data from the cache

Identifying (Pre-)Cacheable Items

Items that don't change that much, non-dynamic:

  • menu
  • about page
  • header
  • footer
  • images
  • javascript files
    -> app shell

What to cache?

  • find out what your app shell is
  • pre-cache that when the service worker is installing
  • what to cache on an on-demand basis (images/articles)
  • cache dynamic content (that changes frequently?

Static Caching at Installation

  • cache files that change that often
    • app shell
    • basic CSS

Example

Store index.html, app.js, app.css, image.png in the cache.
These can be fetched later (from the second-page visit on), even when there is no internet connection.
Cache as much as needed, but not too much.

// sw.js

self.addEventListener("install", event => {
  // without waiting the SW will continue without caching
  event.waitUntil(
    // opens the existing `static` cache or creates it
    caches.open("static").then(cache => {
      console.log("[Service Worker] Precaching Files");
      cache.add("/");
      cache.add("index.html");
      cache.add("app.js");
      cache.add("app.css");
      cache.add("image.png");
      // or better:
      cache.addAll(["/", "index.html", "app.js", "app.css", "image.png"]);
    })
  );
});

Keep in mind you have to store the exact requests you make; including /.
You are caching urls not file paths.

Pre-cache CDN files too (if CORS is supported)

Fetching from Cache

// sw.js

self.addEventListener("fetch", event => {
  event.respondWith(
    // look at all sub caches for a match on the key (= request)
    caches
      .match(event.request)
      // you will always get a response; null if not found
      .then(response => {
        if (response) {
          // if found: return from cache
          return response;
        } else {
          // fetch it from the server
          return fetch(event.request);
        }
      })
  );
});

Dynamic Caching upon Fetching

Resources are fetch because of they are part of your index.html file or a fetch call through JavaScript.
You can store these resources in the cache too.

















 
 
 
 
 
 
 






// sw.js

self.addEventListener("fetch", event => {
  event.respondWith(
    // look at all sub caches for a match on the key (= request)
    caches
      .match(event.request)
      // you will always get a response; null if not found
      .then(response => {
        if (response) {
          // if found: return from cache
          return response;
        } else {
          // fetch it from the server
          return (
            fetch(event.request)
              // and save it to the cache
              .then(res => {
                return caches.open("dynamic").then(cache => {
                  cache.put(event.request.url, res.clone());
                  return res;
                });
              })
          );
        }
      })
  );
});

Add: will send a request to an url and stores it
Put: requires you to provide the request and response to store

Error handling

Add screenshot of SW errors

Prevent offline errors from the fecth event by catching them and ignoring them
























 
 






// sw.js

self.addEventListener("fetch", event => {
  event.respondWith(
    // look at all sub caches for a match on the key (= request)
    caches
      .match(event.request)
      // you will always get a response; null if not found
      .then(response => {
        if (response) {
          // if found: return from cache
          return response;
        } else {
          // fetch it from the server
          return (
            fetch(event.request)
              // and save it to the cache
              .then(res => {
                return caches.open("dynamic").then(cache => {
                  cache.put(event.request.url, res.clone());
                  return res;
                });
              })
              // prevent offline errors from fetch event
              .catch(err => {})
          );
        }
      })
  );
});

Cache versioning

When you precache for instance a CSS file and make changes to that file, the new file will not be loaded from the server but from the cache.

There are two ways to deal with this:

  1. Adjust the Service Worker file a little to force an update (1 byte is enough)
  2. Use sub caches in your Service Worker

Updating the Service Worker without a new cache is not the right approach.
You might mess up the cache the user is using right now with incompatible files.





 





// sw.js

self.addEventListener("install", event => {
  event.waitUntil(
    caches.open("static-v2").then(cache => {
      // save files to the new cache
    })
  );
});

You need to clean-up old versions of your cache because your website might fetch outdated versions of the requested files, because of caches.match(event.request).

Cache Cleanup

As mentioned you don't want to mess with the active cache, so the best moment to cleanup is when the new version of the Service Worker is activated.




 
 
 
 
 
 
 
 
 
 
 
 
 
 
 



// sw.js

self.addEventListener("activate", event => {
  event.waitUntil(
    // will return an array of cache names
    caches.keys().then(keys => {
      // return only when all delete actions resolved a Promise
      return Promise.all(
        // go over all available keys
        keys.map(key => {
          if (key !== "static-v2" && key !== "dynamic") {
            console.log("[Service Worker] Deleting old caches ...", key);
            return caches.delete(key);
          }
        })
      );
    })
  );
  return self.clients.claim();
});

Advanced Caching

Save an article

You can offer the user the opportunity to save an article to the cache to read offline later.

<!-- index.html -->

<button onclick="saveArticle(url)">Save</button>
// app.js

function saveArticle(url) {
  // open or create a new cache for user cache actions
  caches.open("user").then(cache => {
    // save the url(s) to the cache
    cache.add(url);
  });
}

Offline Fallback Page

Create a offline.html with the same app shell and some information.
Add it to the cache in the Service Worker install event.






 




// sw.js

self.addEventListener("install", event => {
  event.waitUntil(
    caches.open("static").then(cache => {
      cache.addAll(["/", "index.html", "offline.html", "app.js", "app.css", "image.png"]);
    })
  );
});

In the Service Worker fetch event add a fallback (a catch) for files that can't be retrieved from the cache nor the internet.
















 
 
 
 
 





// sw.js

self.addEventListener("fetch", event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      if (response) {
        return response;
      } else {
        return fetch(event.request)
          .then(res => {
            return caches.open("dynamic").then(cache => {
              cache.put(event.request.url, res.clone());
              return res;
            });
          })
          .catch(err => {
            return caches.open("static").then(cache => {
              return cache.match("/offline.html");
            });
          });
      }
    })
  );
});

Cache Management

If you have dynamic caching your cache will fill up pretty quickly.
Sometime you might to want to clean up your cache.

// sw.js

// cacheName: which (dynamic) cache?
// maxItems: maximum items you want in your cache
function cleanCache(cacheName, maxItems) {
  caches.open(cacheName).then(cache => {
    return cache.keys().then(keys => {
      // check for the amount of items
      if (keys.length > maxItems) {
        cache
          // delete the oldest item
          .delete(keys[0])
          // recursively call itself until maxItems is reached
          .then(trimCache(cacheName, maxItems));
      }
    });
  });
}

It's up to you when you want to call this method to clean your dynamic cache(s).
The most suitable place would be just before a new item is put in the cache in the fetch event.

Removing a Service Worker

If for some reason you want to get rid of your Service Workers, you can trigger it from your JavaScript code.

// app.js

// get all Service Worker registrations for your domain
navigator.serviceWorker.getRegistrations().then(registrations => {
  // loop through all Service Workers
  for (let registration of registrations) {
    // delete a Service Worker
    registration.unregister();
  }
});

The Service Worker cache(s) will be cleaned for you by this process.