The Fetch Event
The fetch event
Section titled “The fetch event”Once a service worker is activated and controls a page, every network request the page makes — HTML, CSS, JS, images, API calls — passes through the SW’s fetch event. This is the heart of offline-first PWAs.
event.respondWith()
Section titled “event.respondWith()”Inside the fetch listener, call event.respondWith() with a Promise that resolves to a Response. If you do not call event.respondWith(), the request falls through to the network normally (passthrough behaviour).
self.addEventListener('fetch', (event) => { // Simplest possible passthrough — behaves as if no SW is installed event.respondWith(fetch(event.request));});Network-first strategy
Section titled “Network-first strategy”Try the network; if it fails (offline or server error), fall back to the cache.
self.addEventListener('fetch', (event) => { event.respondWith( fetch(event.request) .then((response) => { // Clone the response: a Response body can only be consumed once const clone = response.clone(); caches.open('v1').then((cache) => cache.put(event.request, clone)); return response; }) .catch(() => caches.match(event.request)) );});Cache-first strategy
Section titled “Cache-first strategy”Serve from cache immediately; if not cached, go to the network and cache the result.
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; }); }) );});Stale-while-revalidate strategy
Section titled “Stale-while-revalidate strategy”Return the cached version instantly, then update the cache in the background.
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; }) ) );});Request flow diagram
Section titled “Request flow diagram”sequenceDiagram
participant P as Page
participant SW as Service Worker
participant C as Cache Storage
participant N as Network
P->>SW: fetch(request)
SW->>C: caches.match(request)
alt cache hit
C-->>SW: cached Response
SW-->>P: Response (instant)
else cache miss
SW->>N: fetch(request)
N-->>SW: fresh Response
SW->>C: cache.put(request, response.clone())
SW-->>P: Response
end Pitfalls
Section titled “Pitfalls”Always clone before caching. A Response body is a readable stream that can only be consumed once. If you pass the same Response to both the caller and cache.put(), one of them will receive an empty body. Use response.clone() before storing.
Do not intercept cross-origin opaque responses carelessly. Requests to third-party origins without CORS return an opaque response (type === 'opaque') with status === 0. Caching opaque responses inflates cache storage unpredictably. Apply fetch-event logic only to same-origin requests unless you know what you are doing.
self.addEventListener('fetch', (event) => { // Only handle same-origin requests if (!event.request.url.startsWith(self.location.origin)) return;
event.respondWith( caches.match(event.request).then((cached) => cached || fetch(event.request)) );});