Performance · Concept
Precaching strategies
Published
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
Section titled “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,
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
Section titled “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,
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
Section titled “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,
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
Section titled “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.
Examples
Section titled “Examples”The Workbox form is what most projects ship; the hand-written form shows what it does.
Workbox precacheAndRoute with a generated manifest
Section titled “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.
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
Section titled “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.
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
Section titled “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.
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.
See also
Section titled “See also”- workbox-precaching (developer.chrome.com)
- Service worker caching and HTTP caching (web.dev)
- The app shell model
- CacheStorage and the caches global
- Workbox
- Eviction and best-effort storage
Specifications
| Specification | Status |
|---|---|
| None. | |