Home | Send Feedback | Share on Bluesky |

A closer look at the Cache API

Published: 9. January 2018  •  pwa, javascript

The Cache API is a browser interface that works alongside Service Workers. Its main purpose is to let Service Workers cache network requests so they can return responses to foreground scripts, even when the browser is offline. While it is primarily designed for storing network requests and their responses, you can also use it as a general-purpose storage mechanism.

The main entry point to the API is the global caches object. The API is not limited to Service Workers; you can access caches from a window, a worker, and an iframe. All of them share the same origin-scoped caches.

The Cache API is available only in secure contexts such as HTTPS or localhost. Cache entries do not expire automatically, and the Cache API does not honor HTTP caching headers, so your code is responsible for versioning and cleanup.

CacheStorage

caches is an instance of the CacheStorage type and provides the following five methods. All methods in the Cache API are asynchronous and return a Promise.

open(cacheName)

Opens or creates a cache. An origin can have multiple named caches. If the specified cache does not exist, a new cache is created with the provided cacheName.

const cache = await caches.open('testCache');

has(cacheName)

Checks if a cache with the specified cacheName exists. Returns true if it exists.

const exists = await caches.has('testCache');
// true

delete(cacheName)

Deletes the cache with the specified cacheName. When the cache exists, it deletes the cache and returns true; otherwise, it returns false.

const deleted = await caches.delete('testCache');
// true

keys()

Returns an array containing the names (strings) of all available caches.

const cacheNames = await caches.keys();
// ["testCache"]

match(request, options)

This method is a convenience method that calls the match() method on each existing cache. See the cache.match() description further below. This method processes the caches in the same order that caches.keys() returns the names. The second parameter is optional and supports the same options as cache.match() to control the matching process. This method supports one additional option: cacheName, which specifies the cache to search within.

const todoCache = await caches.open('todoCache');
await todoCache.addAll([
  'https://jsonplaceholder.typicode.com/todos/15',
  'https://jsonplaceholder.typicode.com/todos/16',
  'https://jsonplaceholder.typicode.com/todos/17'
]);

const response1 = await caches.match('https://jsonplaceholder.typicode.com/todos/15', {cacheName: 'todoCache'});
const response2 = await todoCache.match('https://jsonplaceholder.typicode.com/todos/15');

These two match calls are equivalent. caches.match() is useful when you have many caches and need to search for a resource in all of them.

Cache

The Cache object that caches.open() returns provides the following seven methods. All of these methods work with Request and Response objects. Instead of a Request, you can provide a string. The Cache API internally converts the string to a Request object with new Request(string).

put(request, response)

Adds a key-value pair to the cache. put() overwrites an existing entry with the same key.

const todoCache = await caches.open('todoCache');
const url = 'https://jsonplaceholder.typicode.com/todos/23';
const response = await fetch(url);
await todoCache.put(url, response);

add(request) addAll([request, request, request, ...])

Takes one or more URLs, fetches them, and adds the response(s) to the cache.

const todoCache = await caches.open('todoCache');
await todoCache.add('https://jsonplaceholder.typicode.com/todos/24');
await todoCache.addAll([
  'https://jsonplaceholder.typicode.com/todos/25',
  'https://jsonplaceholder.typicode.com/todos/26',
  'https://jsonplaceholder.typicode.com/todos/27'
]);

add() is equivalent to the following code:

  const response = await fetch(url);
  if (response.ok) {
    await cache.put(url, response);
  }

add() and addAll() do not cache responses with a status that is not in the 200 range. put(), on the other hand, stores any request/response pair.


match(request, options)

Returns a Response object associated with the first matching request. Returns undefined when no matching request exists. The second parameter is optional and is an object with options that control the matching process. The following options are supported:


matchAll(request, options)

Works the same as match(), but instead of returning the first response that matches (response[0]), matchAll() returns all matching responses in an array. You can also call matchAll() without any parameters, and the method returns all cached responses.

const todoCache = await caches.open('todoCache');
await todoCache.add('https://jsonplaceholder.typicode.com/todos/30');
await todoCache.add('https://jsonplaceholder.typicode.com/todos/40');
await todoCache.add('https://jsonplaceholder.typicode.com/todos/50');
const first = await todoCache.match('https://jsonplaceholder.typicode.com/todos/30');
// Response {type: "cors", url: "https://jsonplaceholder.typicode.com/todos/30", ...}

const all = await todoCache.matchAll();
/*
[
  Response {type: "cors", url: "https://jsonplaceholder.typicode.com/todos/30", ...}
  Response {type: "cors", url: "https://jsonplaceholder.typicode.com/todos/40", ...}
  Response {type: "cors", url: "https://jsonplaceholder.typicode.com/todos/50", ...}
]
*/

delete(request, options)

Returns true when it found a matching entry and deleted it; otherwise, it returns false.

const todoCache = await caches.open('todoCache');
const request = new Request('https://jsonplaceholder.typicode.com/todos/24');
await todoCache.add(request);
const deleted = await todoCache.delete(request);
// true

This method also takes the same options object as cache.match() as the second optional parameter, which allows you to delete multiple Request/Response pairs for the same URL.


keys(request, options)

Returns an array of keys (Request objects). You can either call the keys() method without a parameter, and it returns all keys that are stored in the cache, or you can specify a request, and the method only returns matching keys. keys() supports the same options as the cache.match() method as the second optional parameter. The keys are returned in the same order that they were inserted.

const todoCache = await caches.open('todoCache');
await todoCache.add('https://jsonplaceholder.typicode.com/todos/30');
await todoCache.add('https://jsonplaceholder.typicode.com/todos/40');
await todoCache.add('https://jsonplaceholder.typicode.com/todos/50');
const allKeys = await todoCache.keys();
 /*
  [
    Request {method: "GET", url: "https://jsonplaceholder.typicode.com/todos/30", ...},
    Request {method: "GET", url: "https://jsonplaceholder.typicode.com/todos/40", ...}
    Request {method: "GET", url: "https://jsonplaceholder.typicode.com/todos/50", ...}
  ]
 */

const oneKey = await todoCache.keys('https://jsonplaceholder.typicode.com/todos/40');
 /*
  [
    Request {method: "GET", url: "https://jsonplaceholder.typicode.com/todos/40", ...}
  ]
 */

General-purpose cache

In the previous section, all the examples used the Fetch API to request a resource from a server and then store the response in the cache. The Cache API is primarily built for this use case and, therefore, can only store a Request object as the key and a Response object as the value.

However, Request and Response objects can contain any data that can be transferred over HTTP. The Response constructor supports different types like Blobs, ArrayBuffers, FormData, and strings.

The following example maps a string key to a string value:

  const languageCache = await caches.open('languages');
  await languageCache.put('de', new Response('German'));
  await languageCache.put('en', new Response('English'));
  await languageCache.put('fr', new Response('French'));

  const allKeys = await languageCache.keys();
  /*
  [
    Request {method: "GET", url: "http://localhost:8100/de", …}
    Request {method: "GET", url: "http://localhost:8100/en", …}
    Request {method: "GET", url: "http://localhost:8100/fr", …}
  ]
  */

  const en = await languageCache.match('en');
  const enText = await en.text();
  // English

  const deleted = await languageCache.delete('fr');
  // true

  const fr = await languageCache.match('fr');
  // undefined

Example with JSON:

  const usersCache = await caches.open('users');
  await usersCache.put('1', new Response('{"id": 1, "name": "John", "age": 27 }'));
  const user1 = await usersCache.match('1');
  const user1json = await user1.json();
  // {id: 1, name: "John", age: 27}

The Response object provides several methods to access the body: arrayBuffer(), blob(), text(), json(), and formData(). Depending on the type of the Response body, you need to call the appropriate method. All these methods are asynchronous and return a Promise.

Example

As mentioned at the beginning of the blog post, the global caches object is not only accessible in the Service Worker; you can also access it from the window. Both contexts share the same origin-scoped caches.

This example shows a practical use case. It is a standalone Ionic/Angular app that fetches and stores four pictures in the Service Worker and then, in the foreground script, accesses the cache and displays the cached pictures.

This could be useful for applications that support an offline mode and need a way to only display items that are cached.

Implementation

The Service Worker listens for the install event and calls loadPictures(). It also listens for the activate event, deletes older images-* caches, and claims existing clients. In loadPictures(), it opens the versioned cache images-v1, fetches and stores four cat pictures with cache.addAll(), and then sends an imagesCached message to all clients associated with this Service Worker.

const CACHE_NAME = 'images-v1';
const PICTURES = [
  'https://cataas.com/cat?width=400&height=300',
  'https://cataas.com/cat?width=500&height=350',
  'https://cataas.com/cat?width=450&height=320',
  'https://cataas.com/cat?width=480&height=360'
];

self.addEventListener('install', event => {
  self.skipWaiting();
  event.waitUntil(loadPictures());
});

self.addEventListener('activate', event => event.waitUntil(activate()));

async function activate() {
  const cacheNames = await caches.keys();
  await Promise.all(cacheNames
    .filter(cacheName => cacheName.startsWith('images-') && cacheName !== CACHE_NAME)
    .map(cacheName => caches.delete(cacheName)));
  await clients.claim();
}

async function loadPictures() {
  const cache = await caches.open(CACHE_NAME);
  await cache.addAll(PICTURES);

  const allClients = await clients.matchAll({includeUncontrolled: true});
  for (const client of allClients) {
    client.postMessage('imagesCached');
  }
}

service-worker.js

In the Ionic app, we call listCache() from ngOnInit(). The first time listCache() runs, the cache can still be empty because the Service Worker may still be installing and populating the cache. To handle that case, the component installs a message event listener and waits for the imagesCached message. As soon as the Service Worker finishes caching the images, it sends that message and the component reloads the cache.

export class HomePage implements OnInit, OnDestroy {
  swiperModules = [IonicSlides];
  pictures: string[] = [];
  private readonly cacheName = 'images-v1';
  private readonly onMessage = (event: MessageEvent<string>) => {
    if (event.data === 'imagesCached') {
      void this.listCache();
    }
  };

  constructor() {
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.addEventListener('message', this.onMessage);
    }
  }

  ngOnInit(): void {
    void this.listCache();
  }

  async listCache(): Promise<void> {
    this.revokeObjectUrls();
    const cache = await caches.open(this.cacheName);
    const responses = await cache.matchAll();
    this.pictures = await Promise.all(
      responses.map(async response => URL.createObjectURL(await response.blob()))
    );
  }

  ngOnDestroy(): void {
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.removeEventListener('message', this.onMessage);
    }
    this.revokeObjectUrls();
  }

  private revokeObjectUrls(): void {
    for (const picture of this.pictures) {
      URL.revokeObjectURL(picture);
    }
    this.pictures = [];
  }

home.page.ts

The listCache() method opens the images-v1 cache, retrieves all entries with matchAll(), converts each Response to a Blob, and creates an object URL for display. On the HTML template, Angular's @for syntax iterates over the picture URLs and renders an img tag for each entry inside a swiper slide.

<ion-content class="ion-padding">
  <swiper-container [modules]="swiperModules" [pagination]="true">
    @for (pic of pictures; track $index) {
      <swiper-slide><img [src]="pic" alt="pic"></swiper-slide>
    }
  </swiper-container>

home.page.html

You can find the source code for this example on GitHub: https://github.com/ralscha/blog/tree/master/sw-cache

For further information about the Cache API, visit these pages: