Concept in Service Worker
Caching strategies for service workers
Published
A caching strategy is the rule a service worker’s fetch handler applies to decide between the Cache Storage copy and the network, and in which order. Five named strategies cover the space; every one of them is a short respondWith() body, and Workbox ships each as a class in workbox-strategies. The underlying primitives (FetchEvent, caches.match(), cache.put()) are Baseline widely available (Chrome 40, Firefox 44, Safari 11.1; BCD api.FetchEvent), so the choice is about trade-offs, not support.
How it works
Section titled “How it works”Each strategy answers the same three questions differently: where the first lookup goes, what happens when that lookup fails, and whether the cache is updated afterwards.
| Strategy | First lookup | On miss or failure | Cache updated | Cost the reader pays |
|---|---|---|---|---|
| Cache First | caches.match() |
fetch(), then cache.put() |
Only on a miss | A stale entry is served until its cache name changes or the entry is deleted |
| Network First | fetch() |
caches.match() |
On every success | One full network wait per request, plus the browser’s offline detection time before the fallback |
| Stale-While-Revalidate | caches.match() |
fetch() |
On every request, in the background | Exactly one response is stale on every request after the first |
| Network Only | fetch() |
Fails | No | No offline behaviour at all |
| Cache Only | caches.match() |
Fails | No | Anything not precached is a hard failure |
Two mechanics apply to every strategy that writes to the cache. A Response body is a stream that can be read once, so the handler must clone() it before passing one copy to cache.put() and returning the other; forgetting the clone throws TypeError: Failed to execute 'put' on 'Cache': Response body is already used in Chromium. And fetch() only rejects on network failure: a 404 or 500 resolves normally, so a strategy that calls cache.put() without checking response.ok caches error pages.
Opaque responses (cross-origin requests made with mode: 'no-cors') have status 0 and an unreadable body. They can be stored, but Chromium pads each one to about 7 MB in quota accounting to prevent size-based cross-origin inference, so a Cache First strategy over third-party fonts or images consumes quota far faster than the bytes transferred suggest.
Network First has a hidden latency problem: when the device is on a captive portal or a dead Wi-Fi link, fetch() does not reject quickly; it waits for the TCP timeout. Add an explicit timeout (AbortSignal.timeout()) and treat it as a miss, which is what Workbox’s NetworkFirst networkTimeoutSeconds option does.
Examples
Section titled “Examples”The hand-written handlers below each implement one row of the table; the last example shows the same routes in Workbox.
Stale-While-Revalidate with the clone done correctly
Section titled “Stale-While-Revalidate with the clone done correctly”The cached copy is returned immediately when it exists; the network response refreshes the cache for the next request. The clone() happens before put() and the returned Response is the original.
self.addEventListener('fetch', (event) => { if (event.request.destination !== 'image') return; event.respondWith((async () => { const cache = await caches.open('images-v1'); const cached = await cache.match(event.request); const network = fetch(event.request).then((response) => { if (response.ok) cache.put(event.request, response.clone()); return response; }); event.waitUntil(network.catch(() => {})); // keep the worker alive for the refresh return cached ?? network; })());});Without event.waitUntil(network) the browser may terminate the worker after respondWith() settles, before the background refresh completes; the catch keeps a network failure from surfacing as an unhandled rejection.
Network First with a timeout and an offline fallback
Section titled “Network First with a timeout and an offline fallback”The handler races the network against a three-second timer; a timeout or a failure falls back to the cache and, for navigations, to a precached /offline.html.
self.addEventListener('fetch', (event) => { if (event.request.mode !== 'navigate') return; event.respondWith((async () => { const cache = await caches.open('pages-v1'); try { const response = await fetch(event.request, { signal: AbortSignal.timeout(3000) }); if (response.ok) cache.put(event.request, response.clone()); return response; } catch { return (await cache.match(event.request)) ?? cache.match('/offline.html'); } })());});AbortSignal.timeout() is available in Chrome 103, Firefox 100, and Safari 16 (BCD api.AbortSignal.timeout_static); in older workers, construct an AbortController and call abort() from setTimeout.
Detecting Cache Storage before relying on a strategy
Section titled “Detecting Cache Storage before relying on a strategy”caches is only exposed in secure contexts (HTTPS and localhost; BCD api.caches), and a page served over plain HTTP has no navigator.serviceWorker.register() either. A page that depends on precached assets should check before registering the worker, and the worker should treat a missing caches as Network Only.
// In the pageif ('serviceWorker' in navigator && 'caches' in window) { navigator.serviceWorker.register('/sw.js');} else { // No Cache Storage: the app runs online-only; do not advertise offline support.}
// In the workerself.addEventListener('fetch', (event) => { if (!('caches' in self)) return; // fall through to the network // …strategy code…});Returning from the handler without calling respondWith() lets the browser perform the request as if no worker existed, which is the correct degraded behaviour.
The same routes in Workbox
Section titled “The same routes in Workbox”Each class in workbox-strategies implements one row of the table above and adds the clone, the ok check (via CacheableResponsePlugin), and the timeout as options.
import { registerRoute } from 'workbox-routing';import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';import { CacheableResponsePlugin } from 'workbox-cacheable-response';
registerRoute(({ request }) => request.mode === 'navigate', new NetworkFirst({ cacheName: 'pages', networkTimeoutSeconds: 3 }));registerRoute(({ request }) => request.destination === 'image', new StaleWhileRevalidate({ cacheName: 'images' }));registerRoute(({ url }) => url.pathname.startsWith('/assets/'), new CacheFirst({ cacheName: 'assets', plugins: [new CacheableResponsePlugin({ statuses: [200] })] }));Without CacheableResponsePlugin, CacheFirst and StaleWhileRevalidate cache any response with status 200 or 0 (opaque) by default; NetworkFirst caches 200 only.
See also
Section titled “See also”- Service Workers specification: FetchEvent (w3.org)
- Workbox strategies module (developer.chrome.com)
- Cache API
- FetchEvent and routing
- Offline fallback
- Workbox
Specifications
| Specification | Status |
|---|---|
| None. | |