LEARNING OBJECTIVES ⌵
- Understand how the Service Worker
fetchevent intercepts and proxies HTTP traffic between the browser and network. - Master the 5 fundamental caching patterns: Cache-First, Network-First, Stale-While-Revalidate, Network-Only, and Cache-Only.
- Implement stream cloning mechanics (
response.clone()) to safely satisfy both the browser rendering engine andCacheStorage. - Build a multi-strategy dynamic router that matches request types (HTML, JS/CSS bundles, API JSON, static images) to their optimal caching strategy.
📖 The Mental Model & Story (Intuitive Foundation)
Imagine a high-end chef working in a busy restaurant kitchen. When an order arrives:
- For salt, pepper, and flour (static CSS/JS assets that never change), the chef immediately reaches into the kitchen pantry (Cache-First). They do not call the distributor every time a pinch of salt is needed.
- For live lobster or fresh fish (account balances and critical financial APIs), the chef calls the harbor immediately (Network-First). If the harbor is closed or the line is dead, only then do they check the freezer as an emergency fallback.
- For daily newspaper or daily specials board (avatars, articles, social feeds), the chef quickly reads yesterday's board to the customer while sending an apprentice out the back door to fetch the latest edition (Stale-While-Revalidate).
- For credit card payment processing (POST checkout mutations), the chef only communicates through the bank terminal and never attempts to cache the transaction (Network-Only).
In a Progressive Web App, your Service Worker is the chef. It routes every inbound network request to the appropriate storage strategy based on the data's freshness requirements and performance profile.
Technical Deep Dive & Specifications
The 5 Core Caching Strategies
+----------------------------------------------------------------------------------------------------+
| 1. CACHE-FIRST (Cache Falling Back to Network) |
| Best for: Hashed static assets (bundle.8a7f.js, styles.css, fonts, webp images) |
| Request ===> Check Cache? ===[Hit]===> Return Cached Response |
| | |
| [Miss] ===> Fetch from Network ===> Put into Cache ===> Return Network Response |
+----------------------------------------------------------------------------------------------------+
+----------------------------------------------------------------------------------------------------+
| 2. NETWORK-FIRST (Network Falling Back to Cache) |
| Best for: Frequently updated data (user inbox, real-time analytics, dynamic HTML) |
| Request ===> Fetch Network ===[Success]===> Update Cache ===> Return Network Response |
| | |
| [Failure/Offline] ===> Return Cached Response (Fallback) |
+----------------------------------------------------------------------------------------------------+
+----------------------------------------------------------------------------------------------------+
| 3. STALE-WHILE-REVALIDATE (Instant Cache + Background Network Refresh) |
| Best for: Non-critical dynamic content (avatars, news lists, category feeds) |
| Request ===> Return Cached Response IMMEDIATELY (Fastest UX) |
| +======> [Background Thread] Fetch Network ===> Update Cache for Next Visit |
+----------------------------------------------------------------------------------------------------+
+----------------------------------------------------------------------------------------------------+
| 4. NETWORK-ONLY | 5. CACHE-ONLY |
| Best for: Non-idempotent POST/PUT, payment APIs | Best for: Fixed App Shell & offline page |
| Request ===> Network (Bypasses Cache entirely) | Request ===> CacheStorage (Never touches |
| | network) |
+----------------------------------------------------------------------------------------------------+
Comparison Matrix
| Strategy | Performance / Latency | Freshness | Offline Support | Recommended Use Cases |
|---|---|---|---|---|
| Cache-First | ⚡ Instant (0–10ms) | Low (until invalidated) | 🟢 100% | Web fonts, versioned JS/CSS, brand logos, static SVG icons. |
| Network-First | ⏳ Network bound (100–1000ms) | 🟢 Real-time | 🟡 Fallback only | HTML navigation documents, user balance APIs, stock tickers. |
| Stale-While-Revalidate | ⚡ Instant (0–10ms) | 🟡 Near real-time | 🟢 100% | User profiles, article feeds, product catalogs, dashboard metrics. |
| Network-Only | ⏳ Network bound | 🟢 Real-time | 🔴 None (fails offline) | Checkout payments, authentication tokens, live chat sockets. |
| Cache-Only | ⚡ Instant (0–5ms) | Fixed at install | 🟢 100% | Offline fallback HTML, isolated embedded offline help docs. |
The Response Stream Cloning Rule (response.clone())
The Fetch API Response object is a single-use readable stream (ReadableStream). Once the browser consumes the stream to paint the UI or parse JSON, the stream is locked and cannot be read a second time.
+-----------------------------+
| Network Fetch Response |
+-----------------------------+
|
[ Call response.clone() ]
|
+-----------------+-----------------+
| |
v v
+---------------------+ +---------------------+
| Stream 1: Original | | Stream 2: Clone |
| Return to Browser | | Put into Cache |
| window for DOM | | storage bucket |
+---------------------+ +---------------------+
// MUST clone response before putting it into cache!
const networkResponse = await fetch(event.request);
const clonedResponse = networkResponse.clone();
await cache.put(event.request, clonedResponse);
return networkResponse;
💻 Interactive Code Playground
Starter Code: Production Multi-Strategy Router
Below is a complete, modular Service Worker demonstrating dynamic strategy routing based on request destination and URL patterns.
Line-by-Line Code Breakdown
- Line 37 (
request.destination): Uses standard Fetch metadata ('style','script','font','image') to cleanly route static resources without brittle regex file extensions. - Line 43 (
url.pathname.startsWith('/api/feed')): Routes API read feeds through Stale-While-Revalidate for instantaneous UI response. - Line 49 (
request.mode === 'navigate'): Identifies browser address bar URL changes and page reloads to execute Network-First with App Shell fallback. - Line 66 (
cache.put(request, networkResponse.clone())): Clones the response stream before persisting it inCacheStorage, allowing the original stream to be returned to the DOM. - Line 104 (
return cachedResponse || networkFetch): Implements the Stale-While-Revalidate magic: returns cached bytes immediately while the background promisenetworkFetchupdates storage concurrently.
Expected Browser Render Output
// sw.js - Production Multi-Strategy Routing Engine
const STATIC_CACHE = 'static-assets-v1';
const DYNAMIC_CACHE = 'dynamic-content-v1';
// 1. Install & Precache core assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(STATIC_CACHE).then((cache) => {
return cache.addAll([
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js'
]);
})
);
self.skipWaiting();
});
// 2. Activate & Purge
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => {
return Promise.all(
keys.map((key) => {
if (key !== STATIC_CACHE && key !== DYNAMIC_CACHE) {
return caches.delete(key);
}
})
);
}).then(() => self.clients.claim())
);
});
// 3. Dynamic Strategy Router
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Strategy A: Cache-First for static assets (CSS, JS, Fonts, Images)
if (['style', 'script', 'font', 'image'].includes(request.destination)) {
event.respondWith(cacheFirstStrategy(request));
return;
}
// Strategy B: Stale-While-Revalidate for API Feed requests
if (url.pathname.startsWith('/api/feed')) {
event.respondWith(staleWhileRevalidateStrategy(request));
return;
}
// Strategy C: Network-First for HTML navigation and user profiles
if (request.mode === 'navigate' || url.pathname.startsWith('/api/user')) {
event.respondWith(networkFirstStrategy(request));
return;
}
// Default: Network Only
event.respondWith(fetch(request));
});
// Strategy Implementation: CACHE-FIRST
async function cacheFirstStrategy(request) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
try {
const networkResponse = await fetch(request);
if (networkResponse.status === 200) {
const cache = await caches.open(STATIC_CACHE);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (err) {
return new Response('Asset unavailable offline', { status: 408 });
}
}
// Strategy Implementation: NETWORK-FIRST
async function networkFirstStrategy(request) {
try {
const networkResponse = await fetch(request);
if (networkResponse.status === 200) {
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (err) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Fallback for HTML navigations
if (request.mode === 'navigate') {
return caches.match('/index.html');
}
return new Response(JSON.stringify({ error: 'Offline', offline: true }), {
headers: { 'Content-Type': 'application/json' },
status: 503
});
}
}
// Strategy Implementation: STALE-WHILE-REVALIDATE
async function staleWhileRevalidateStrategy(request) {
const cache = await caches.open(DYNAMIC_CACHE);
const cachedResponse = await cache.match(request);
// Background fetch to refresh the cache
const networkFetch = fetch(request).then((networkResponse) => {
if (networkResponse.status === 200) {
cache.put(request, networkResponse.clone());
}
return networkResponse;
}).catch(() => {
// Network failed, silent catch for background revalidation
});
// Return cached version immediately if present, otherwise await network
return cachedResponse || networkFetch;
}(First Page Load - Online):
[Network] GET /styles/main.css -> 200 OK (Downloaded & Cached in static-assets-v1)
[Network] GET /api/feed -> 200 OK (Downloaded & Cached in dynamic-content-v1)
(Second Page Load - Instantaneous):
[SW] GET /styles/main.css -> (Served from ServiceWorker Cache: 2ms)
[SW] GET /api/feed -> (Served from ServiceWorker Cache: 3ms)
[SW Background] GET /api/feed -> 200 OK (Cache updated silently for next visit)
(Third Page Load - Airplane Mode / Offline):
[SW] GET /index.html -> (Network failed -> Fallback to cached index.html)
[SW] GET /styles/main.css -> (Served from ServiceWorker Cache)🏋️ Hands-On Exercise
🎯 The Challenge: Build a Stale-While-Revalidate Image Cache
Instructions:
- Intercept any
fetchevent whose URL ends in.jpg,.png, or.webp. - Open a cache bucket named
'image-store-v1'. - Check the cache: if an image exists, return it immediately.
- Concurrently fire a network
fetch(), update the cache with the cloned response upon success, and ensure no unhandled promise rejections occur if the network is down.
🏁 Starter Code Sandbox
⚠️ Common Pitfalls
- Caching POST, PUT, or DELETE Requests in CacheStorage: The
CacheStorageAPI only supportsGETrequests according to the W3C specification. Callingcache.put()with aPOSTrequest throws an unhandledTypeError: Request method 'POST' is unsupported. - Forgetting to Clone Responses: Calling
cache.put(request, response)without callingresponse.clone()consumes the body stream, causingevent.respondWith(response)to fail withTypeError: Already read. - Caching Opaque Cross-Origin Responses blindly: When fetching non-CORS cross-origin resources (
mode: 'no-cors'), the browser returns an opaque response (status0). Opaque responses consume a massive padded quota (often 7MB+ per response in Chromium) regardless of true file size.
💡 Pro Tips
- Apply Network Timeouts to Network-First: On high-latency or unstable mobile connections ("Lie-Fi"), a standard
fetch()can take 30–60 seconds before failing. Wrap yourfetch()in aPromise.race()with a 3-second timeout that falls back to cache if the server does not respond quickly. - Enforce Cache Limits via LRU Eviction: Unchecked dynamic caches will grow indefinitely. Implement a Least-Recently-Used (LRU) cleanup helper that deletes the oldest entries when a cache bucket exceeds a threshold (e.g., 50 items).
📌 Key Takeaways
- The
fetchevent turns the Service Worker into a client-side programmable network proxy. - Cache-First provides the fastest possible load times for immutable, hashed static assets.
- Network-First guarantees fresh content while providing an offline safety net for essential data.
- Stale-While-Revalidate provides zero-latency responses while refreshing storage asynchronously in the background.
Responsestreams are single-use; you must callresponse.clone()before writing toCacheStorage.- --