JavaScript's Cache API is Underrated

ยท

5 min read

Did you know the Cache API is available outside of Service Worker?

It is and that means your app doesn't need to be a PWA to benefit from this feature. Let's look at a quick intro of the API then some examples for inspiration.

Cache API Intro

The Cache API is for saving request/response pairs only. It is managed via the global caches property, which is an instance of CacheStorage. You can create multiple caches then read, update, and delete them at any time. Like localStorage, you get lots of storage space and the cache persists between browser sessions.

Below is just a few of the primary methods of the API. Start with MDN: caches global property to do a deep-dive of the complete API.

Global caches methods (the CacheStorage interface):

open(cacheName) - Finds the named Cache object or creates it if it doesn't exist, then returns a Promise with that cache. The cache name can be used as a versioning mechanism if needed.

keys() - Returns a Promise that resolves to an array of all your cache names.

Cache instance methods:

add(request) - Sends the request and puts a successful response in the cache and returns an empty Promise. This a shortcut for fetching, checking the response is 200-level, and saving that request/response pair with cache.put(request, response).

put(request, response) - Adds a request/response pair to the cache and returns an empty Promise. Unlike the add method, you have to write the fetch and decide if the response meets your needs, i.e. put doesn't check response.ok for you.

match(request, options) - Gets the response of the first matching request and returns it in a Promise.

delete(request, options) - Deletes this entry and returns a Promise with true if the cache entry was deleted or false if not.

Note: The request param of all these methods is not just a Request object. Like fetch, it can be a string, a URL object, a Request object, or even something with a stringifier that returns a URL, like the HTMLAnchorElement.

Examples

JSON and localStorage

Fetching JSON data then storing it in local storage is quite common. With the cache API you store the actual response objects, which means there's no need to stringify and parse JSON. And it's cleaner, has better performance, and keeps you in a simple request/response paradigm.

Before (Storage API)

// You'll use fetch, Response, JSON, and localStorage:

// Fetch and store data
const res = await fetch('/products');
const products = await res.json();
const json = JSON.stringify(products);
localStorage.setItem('products', json);

// Retrieve the data later
const json = localStorage.getItem('products');
const products = JSON.parse(json);

After (Cache API)

// You'll use just Cache and Response:

// Fetch and store data
const cache = await caches.open('products');
await cache.add('/products');

// Retrieve the data later
const res = await cache.match('/products');
const products = await res.json()

Caching assets with missing headers

If one or more of your app's assets is missing good cache headers, and you can't get that fixed on the server, you can use the Cache API to "fix" it:

// Use a cache for assets with unfixable headers
const assets = await caches.open('assets');
let res = await cache.match('/photo.jpg');

if (!res) {
  await cache.add('/photo.jpg');
  res = await cache.match('/photo.jpg');
}

// Set this on an <img> source
const imgSrc = await res.blob().then(blob => URL.createObjectURL(blob));

Typeahead cache

If you've ever built a typeahead component, then you probably used a simple object as an in-memory cache to make the UX faster and avoid hammering the server with queries. Using a Cache makes that a little more robust as the results are not lost after the page is closed. If your typeahead results are relevant on subsequent site visits, consider a cache:

class Typeahead {
  static #cache = await caches.open('typeahead');

  submitQuery(query) {
    const url = `https://example.com/query/${query}`;

    // Try cache
    let res = await this.#cache.match(url);
    if (!res) {
      // Fetch and cache
      await this.#cache.add(url);
      res = await this.#cache.match(url);
    }

    const results = await res.json();
    // Do stuff with the results...
  }
}

Batching requests

Use Cache's addAll to send multiple requests. It also checks response.ok internally and will save successful responses. You know, even if the caching aspect is not really needed, there's some nice shortcuts here...๐Ÿค”

A good example is bootstrapping app data:

// Fetch and cache the app's core data
const urls = ['/user', '/account', '/cart'];
const appCache = await caches.open('app_data');
await appCache.addAll(urls); // Three requests cached

Optimistic pre-fetch

Some UX flows can be optimized by fetching the resources required in the next step. This can be done with regular fetch and saving the results in memory, but the global access caches affords makes it much easier to manage. Imagine a multi-page experience where actions on one page determine what will be needed on the next page:

// Multi-step experience with optimistic pre-fetch
const cache = await caches.open('buy_flow');

// 1.
// User is creating an account. 
// The next step in the flow is answering a questionaire,
// so we pre-fetch the questionaire data.
await cache.add('/questionaire');

// User clicks next, questionaire instantly renders.

// 2.
// User reaches a deterministic spot in the questionaire. 
// The next step in the flow is picking a plan,
// so we pre-fetch the relevant plans.
await cache.add('/plans');

// User clicks next, plans instantly render.

// 3.
// User is picking a plan. 
// The next step in the flow is checkout,
// so we fetch add-ons and payment methods.
await cache.addAll(['/addons', '/payment-methods']);

// User clicks next and checkout instantly renders.

// 4.
// User confirms order and submits payment.
// $$$

No matter the framework, architecture, or dependencies, every page/component in this flow is guaranteed direct access to caches.open('buy_flow'). This is a much simpler and more robust design than using fetch alone or requiring a shared 3rd-party dependency across the experience.

Using the platform

The Cache API is a powerful tool available to every web page - not just PWAs! Hopefully this intro and examples has inspired you to leverage the native web platform more. If you have any non-PWA examples of using the Cache API to make your project better, please share!

ย