Choosing a caching strategy
Published Updated
In one line: the right caching strategy depends on whether the resource is static, dynamic, or time-sensitive — and how tolerant your users are of stale content.
The core strategies
Section titled “The core strategies”Service workers intercept fetch events and decide where to get the response: the cache,
the network, or both. Each strategy makes a different trade-off between speed, freshness,
and offline availability.
Cache-first
Section titled “Cache-first”Check the cache first; fall back to the network if nothing is cached.
async function cacheFirst(request) { const cached = await caches.match(request); if (cached) return cached; return fetch(request);}Use for: static assets (JS bundles, CSS, images, fonts) that don’t change between deploys. Once cached, responses are instant — even offline.
Network-first
Section titled “Network-first”Try the network first; fall back to the cache if the request fails or times out.
async function networkFirst(request) { try { const response = await fetch(request); const cache = await caches.open('dynamic'); cache.put(request, response.clone()); return response; } catch { return caches.match(request); }}Use for: dynamic content (API responses, user data) where freshness matters but offline fallback is still valuable.
Stale-while-revalidate
Section titled “Stale-while-revalidate”Return the cached response immediately, then fetch a fresh copy in the background for next time.
async function staleWhileRevalidate(request) { const cached = await caches.match(request); const fetchPromise = fetch(request).then((response) => { const cache = caches.open('dynamic'); cache.then((c) => c.put(request, response.clone())); return response; }); return cached || fetchPromise;}Use for: semi-static content (UI chrome, article lists) where showing something quickly is more important than showing the absolute latest version.
Network-only
Section titled “Network-only”Always go to the network, never the cache.
Use for: non-GET requests (POST, PUT, DELETE), real-time data (WebSocket connections), or resources that must never be stale.
Cache-only
Section titled “Cache-only”Serve exclusively from the cache; fail if the resource is not cached.
Use for: pre-cached resources during an install event (app shell, offline fallback page).
Choosing a strategy
Section titled “Choosing a strategy”| Resource type | Recommended strategy | Why |
|---|---|---|
| App shell (HTML, CSS, JS) | Cache-first | Rarely changes; instant load after first visit |
| Images and fonts | Cache-first | Immutable between deploys when hashed filenames are used |
| API data (user-specific) | Network-first | Must be fresh; cache provides offline fallback |
| API data (shared/semi-static) | Stale-while-revalidate | Show cached data fast, update in background |
| Navigation requests | Network-first with offline fallback | Fresh HTML when online; cached shell when offline |
Time-based expiration
Section titled “Time-based expiration”For resources that are neither immutable nor critical, add a time check to stale-while- revalidate or cache-first. Store the response timestamp alongside the cached response and fall back to the network if the cache is older than your threshold:
async function cacheWithExpiry(request, maxAgeSeconds) { const cached = await caches.match(request); if (cached) { const cachedDate = new Date(cached.headers.get('date')); if (Date.now() - cachedDate.getTime() < maxAgeSeconds * 1000) { return cached; } } return fetch(request);}Practical checklist
Section titled “Practical checklist”- Use cache-first for hashed static assets (JS, CSS, images).
- Use network-first for API data and user-specific content.
- Use stale-while-revalidate for semi-static lists and UI chrome.
- Add a precache step during the
installevent for the app shell. - Set a cache name that includes a version string so deploys bust old caches.
- Limit cache size and expiry to prevent unbounded storage growth.
- Always provide an offline fallback page for navigation requests.