Skip to content

Caching strategies for service workers

In one line: A caching strategy is the rule your service worker follows when a fetch event fires — whether to go to the cache, the network, or both, and in what order. The five standard strategies cover every offline and performance trade-off a PWA needs.

Cache First (cache falling back to network)

Section titled “Cache First (cache falling back to network)”

Check the cache first. On a hit, return the cached response. On a miss, fetch from the network and optionally store the result.

Best for: Static assets with long-lived content (app shell, fonts, images, versioned JS bundles). Cache hits are instant; misses pay the full network round-trip.

Avoid for: Any resource that must always be fresh (user data, prices, auth tokens).

self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) return cached;
return fetch(event.request).then((response) => {
const clone = response.clone();
caches.open('v1').then((cache) => cache.put(event.request, clone));
return response;
});
})
);
});

Network First (network falling back to cache)

Section titled “Network First (network falling back to cache)”

Try the network. On success, cache and return the fresh response. On network failure, fall back to whatever is cached.

Best for: Pages that should be current but tolerate a stale offline fallback (news feeds, dashboards).

Avoid for: Large, infrequently changing assets — every request pays the network penalty.

self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
const clone = response.clone();
caches.open('v1').then((cache) => cache.put(event.request, clone));
return response;
})
.catch(() => caches.match(event.request))
);
});

Return the cached version immediately (fast), then fetch a fresh copy in the background and update the cache for next time.

Best for: Resources where speed matters more than absolute freshness (avatar images, non-critical API data, secondary scripts). The user always sees something fast; the background fetch keeps the cache warm.

Avoid for: Resources where the stale copy can cause errors if loaded alongside fresher content in the same page load.

self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open('v1').then((cache) =>
cache.match(event.request).then((cached) => {
const networkFetch = fetch(event.request).then((response) => {
cache.put(event.request, response.clone());
return response;
});
return cached ?? networkFetch;
})
)
);
});

Always go to the network. The service worker does not cache anything.

Best for: Requests that must never be served stale: analytics pings, POST mutations, payment requests. Effectively a passthrough.

Only serve from the cache. If nothing is cached, the request fails.

Best for: Resources that were pre-cached during install and are guaranteed to exist (the offline shell). Never use for dynamic or user-specific content.

Resource type Recommended strategy
App shell (HTML, CSS, core JS) Cache First with version-keyed cache names
Versioned static assets (/assets/main.abc123.js) Cache First — the URL itself changes on deploy
API responses, pages that should be current Network First with offline fallback
Avatars, thumbnail images, secondary fonts Stale-While-Revalidate
Auth, payments, analytics Network Only
Offline fallback page Cache Only (pre-cached in install)

Workbox implements all five strategies as composable classes, removing the need for hand-rolled fetch event handlers:

import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
// App shell — cache first, only cache 200 responses
registerRoute(
({ request }) => request.mode === 'navigate',
new NetworkFirst({ cacheName: 'pages' })
);
// Images — stale-while-revalidate
registerRoute(
({ request }) => request.destination === 'image',
new StaleWhileRevalidate({ cacheName: 'images' })
);
// Versioned assets — cache first, long-lived
registerRoute(
({ url }) => url.pathname.startsWith('/assets/'),
new CacheFirst({
cacheName: 'static-assets',
plugins: [new CacheableResponsePlugin({ statuses: [200] })],
})
);

See Workbox for full setup details.

  • Choose a strategy per resource type, not one strategy for everything.
  • Version cache names (e.g. shell-v3) and delete old caches in activate.
  • Never cache non-idempotent requests (POST, PUT, DELETE) unless you’re intentionally building a background sync queue.
  • For Cache First assets, tie the URL to the content hash so stale content is never served from cache after a deploy.
  • Add an explicit offline fallback page so Network First misses produce a usable response, not a browser error. See Offline fallback.