Using the Cache API for Offline Data

The Cache API provides a storage system for network requests and their corresponding responses. While it was originally designed to help service workers serve fast responses regardless of network conditions, it also works as a general-purpose storage mechanism in any modern browser.

Availability and Storage

The API is exposed through the global caches property and can be accessed from a window, iframe, worker, or service worker. You can check for its presence with simple feature detection:

if ('caches' in window) {
  // Cache API is available
}

Caches store pairs of Request and Response objects, but these can carry any kind of data transmittable over HTTP. Storage limits vary by browser implementation, though you should typically expect at least a couple hundred megabytes, with the actual cap usually tied to the device's available storage.

Creating and Opening Caches

Use caches.open(name) to open a cache. If a cache with that name doesn't exist, it will be created. The method returns a Promise that resolves with the Cache object:

const cache = await caches.open('my-cache');

Adding Entries

Three methods add items to a cache: add, addAll, and put. Each returns a Promise.

cache.add

cache.add() takes either a Request object or a URL string, fetches it from the network, and stores the response. If the fetch fails or returns a non-200 status code, nothing is stored and the promise rejects. Note that cross-origin requests not in CORS mode return a status of 0 and thus cannot be stored with this method—use put() for those instead.

await cache.add('/data.json');

cache.addAll

Like add(), but accepts an array of Request objects or URL strings. The returned promise rejects if any single request cannot be cached:

await cache.addAll(['/css/styles.css', '/js/app.js']);

Adding a new entry will overwrite any existing matching entry based on the same matching rules used for retrieval.

cache.put

cache.put() is more flexible. It accepts a Request object or URL string as the first parameter and a Response object as the second. The response can come from the network or be generated by your code, which means you can store non-CORS responses and responses with any status code. Any previous response for the same request is overwritten.

// Store a generated response
const request = new Request('/my-data');
const response = new Response('<p>Hello World</p>', {
  headers: {'Content-Type': 'text/html'}
});
await cache.put(request, response);

The Response constructor accepts several data types: Blobs, ArrayBuffers, FormData objects, and strings. Set the MIME type via the appropriate header on the response:

const response = new Response('<p>Hello World</p>', {
  headers: {'Content-Type': 'text/html'}
});

If you have a stored response and need to read its body, use one of the response helper methods—json(), text(), blob(), or similar—each of which returns a Promise resolving to the body in that format:

const response = await cache.match(request);
const data = await response.json();

Retrieving Cached Items

The match method on a cache finds an entry:

const cachedResponse = await cache.match('/data.json');

If a URL string is passed, the browser converts it to a Request via new Request(request). The method resolves to a Response if a match is found, or undefined if not. Matching considers more than just the URL: different query strings, Vary headers, or HTTP methods (GET, POST, PUT, etc.) mean the requests are considered different.

Pass an options object as a second parameter to ignore some or all of those differentiating factors:

const options = {
  ignoreSearch: true,
  ignoreMethod: true,
  ignoreVary: true
};

const cachedResponse = await cache.match('/data', options);

When multiple entries match, the first one created is returned. Use cache.matchAll() to retrieve all matching responses. For a shortcut that searches across every cache at once, call caches.match() instead:

const cachedResponse = await caches.match('/data.json');

Searching a Cache

The Cache API offers no direct search mechanism beyond matching against a Response. Two practical alternatives exist for implementing your own search.

Filtering entries

Iterate over all entries and filter by any property of the Request or Response objects. For example, to find all cached items whose URLs end in .png:

const keys = await cache.keys();
const pngEntries = await Promise.all(
  keys.filter(key => key.url.endsWith('.png'))
    .map(key => cache.match(key))
);

This approach is simple but can become slow over large data sets.

Maintaining an index

For better performance with many entries, maintain a separate searchable index in IndexedDB, which is designed for this kind of operation. Store each request's URL alongside the searchable properties so you can quickly find and retrieve the matching cache entry after a search.

Deleting Items and Caches

To delete an individual entry:

const deleted = await cache.delete('/data.json');

The request parameter can be a Request object or a URL string. The method also accepts the same options object as cache.match, letting you delete multiple request/response pairs for the same URL:

const deleted = await cache.delete('/data', {ignoreSearch: true});

To remove an entire cache, call caches.delete(name). This returns a Promise that resolves to true if the cache existed and was deleted, or false otherwise:

const wasDeleted = await caches.delete('my-cache');