# Precaching strategies

> How precaching fills Cache Storage with a build-time list of shell assets at install, why each entry needs a revision or hashed URL, and how Workbox does it.

Precaching stores a fixed, build-time list of files in Cache Storage during the service
worker's `install` event, before any page requests them, so the app shell is on the device
from the first visit onwards and loads offline. It is the counterpart of runtime caching, which
stores responses as they are requested, and the two are used together: precache the shell,
runtime-cache the content.

## How it works

The install handler calls `cache.addAll()` with the list; the promise rejects if any entry fails,
and the worker does not install, which keeps a half-filled shell from going live. Each later
deploy ships a new list. The hard part is change detection: a file at an unchanged URL
(`/index.html`) looks identical to the cache even when its contents differ, so every entry needs
either a content hash in its URL (`/app.3f2a1c.js`), which makes a changed file a new URL, or a
separate revision string that the worker compares on install
([workbox-precaching](https://developer.chrome.com/docs/workbox/modules/workbox-precaching),
developer.chrome.com). Maintaining revisions by hand is the error-prone step, which is why a
build plugin generates the manifest.

### What belongs in the precache

The shell: the HTML that renders the frame, its CSS and JavaScript chunks, the icons and the
offline page. Content that varies per user or per request, large media, and anything the user
may never open belong to runtime caching under a strategy
([caching strategies overview](https://developer.chrome.com/docs/workbox/caching-strategies-overview),
developer.chrome.com). A precache that grows into megabytes slows every install and, on Chrome,
delays the first visit's own requests while `addAll()` competes for bandwidth; the shell of a
typical app is a few hundred kilobytes.

### Interaction with the HTTP cache

The install-time `fetch()` for each entry goes through the HTTP cache. A file served with a
long `max-age` at an unchanged URL can therefore land in the precache as a stale copy even
though the revision changed; the server-side rule is long `max-age` only for hashed URLs and
`no-cache` (revalidate) for unhashed ones such as the HTML and the service worker script itself
([Service worker caching and HTTP caching](https://web.dev/articles/service-worker-caching-and-http-caching),
web.dev). Workbox requests unhashed entries with `cache: 'reload'` semantics by appending the
revision as a `__WB_REVISION__` query parameter, which bypasses a stale HTTP cache entry without
changing the URL the page requests.

### Support position

Precaching uses only `caches.open()`, `cache.addAll()`, and the `install` and `activate` events,
present in Chrome 40, Firefox 44, Safari 11.1, and every current engine (BCD `api.CacheStorage`,
`api.ServiceWorkerGlobalScope`). Safari's seven-day eviction of script-writable storage removes
the precache for sites the user has not interacted with, after which the next visit reinstalls
it; see [eviction and best-effort storage](/reference/storage/eviction/).

## Examples

The Workbox form is what most projects ship; the hand-written form shows what it does.

### Workbox precacheAndRoute with a generated manifest

The build tool replaces `self.__WB_MANIFEST` with the `{ url, revision }` list; `revision` is
`null` for hashed URLs and a content hash otherwise. `precacheAndRoute()` installs, serves, and
cleans up.

```js
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';

cleanupOutdatedCaches();
precacheAndRoute(self.__WB_MANIFEST);
// Generated at build time, for example:
// [{ url: '/index.html', revision: '383676' }, { url: '/app.3f2a1c.js', revision: null }]
```

`cleanupOutdatedCaches()` removes precaches left by older Workbox versions; the current
precache's own stale entries are removed during `activate` without it.

### A hand-written precache with revisions

Without Workbox the same mechanism is a map of URL to revision and an install handler that
re-fetches only changed entries, which keeps the second install from re-downloading the whole
shell.

```js
const PRECACHE = 'precache-v2';
const MANIFEST = { '/index.html': 'a1b2c3', '/app.3f2a1c.js': null, '/offline.html': 'd4e5f6' };

self.addEventListener('install', (event) => {
  event.waitUntil((async () => {
    const cache = await caches.open(PRECACHE);
    for (const [url, revision] of Object.entries(MANIFEST)) {
      const key = revision ? `${url}?__rev=${revision}` : url;
      if (!(await cache.match(key))) {
        await cache.put(key, await fetch(key, { cache: 'reload' }));
      }
    }
  })());
});
```

The `fetch` handler looks up `${url}?__rev=${revision}` for a requested `url`; the `{ cache:
'reload' }` option forces a network fetch past the HTTP cache for the changed entry.

### Detecting a usable Cache Storage before relying on the precache

A page can tell whether the shell is precached and otherwise keep its network-first behaviour,
for instance to decide whether to show an "Available offline" indicator.

```js
async function shellIsPrecached() {
  if (!('caches' in window) || !('serviceWorker' in navigator)) {
    return false; // no Cache Storage: nothing is precached
  }
  const names = await caches.keys();
  const precache = names.find((n) => n.startsWith('workbox-precache') || n.startsWith('precache-'));
  if (!precache) return false;
  const cache = await caches.open(precache);
  return Boolean(await cache.match('/index.html', { ignoreSearch: true }));
}
```

`ignoreSearch: true` matters because the stored key carries the revision query parameter;
without it the lookup for `/index.html` misses.

:::observed
Workbox's precache is a cache named from `cacheNames.precache` in `workbox-core`, by default
`workbox-precache-v2-` followed by the registration scope, so Chrome DevTools, Application >
Storage > Cache storage, shows an entry such as `workbox-precache-v2-https://example.com/` whose
rows for unhashed files carry a `?__WB_REVISION__=<hash>` suffix in the **Name** column while
hashed files are stored under their plain URL ([workbox-core](https://developer.chrome.com/docs/workbox/modules/workbox-core),
developer.chrome.com). After a deploy that changes `index.html`, the row's suffix changes and
the old row is gone once the new worker has activated.
:::

## See also

- [workbox-precaching](https://developer.chrome.com/docs/workbox/modules/workbox-precaching) (developer.chrome.com)
- [Service worker caching and HTTP caching](https://web.dev/articles/service-worker-caching-and-http-caching) (web.dev)
- [The app shell model](/reference/performance/app-shell/)
- [CacheStorage and the caches global](/reference/storage/cache-storage/)
- [Workbox](/reference/service-worker/workbox/)
- [Eviction and best-effort storage](/reference/storage/eviction/)