Service Worker & PWA
A service worker is a network proxy that runs in its own thread, independent of pages. It enables offline, fine-grained caching, push, and background sync — but it's powerful and easy to footgun (stale content, "won't update"). Treat versioning and updates as first-class.
Lifecycle (internalize this)
register → install → activate → (controls pages) → update
- install: precache the app shell. Call
self.skipWaiting() to activate immediately (only if you handle updates gracefully).
- activate: clean up old caches. Call
self.clients.claim() to control existing pages.
- A new SW waits until all old tabs close unless you
skipWaiting(). This is why "my change didn't show up".
- Scope = the SW's directory and below. Serve it from the root to control the whole origin.
navigator.serviceWorker.register('/sw.js'); // page side
// sw.js
const CACHE_PREFIX = 'my-app-shell-'; // unique to this app on the origin
const VERSION = `${CACHE_PREFIX}v3`;
const SHELL_ASSETS = ['/offline.html']; // include every offline fallback
self.addEventListener('install', (e) => {
e.waitUntil(caches.open(VERSION).then((c) => c.addAll(SHELL_ASSETS)));
});
self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k.startsWith(CACHE_PREFIX) && k !== VERSION)
.map((k) => caches.delete(k))),
),
);
});
Caching strategies — match to content type
| Strategy |
Use for |
Behavior |
| Cache-first |
hashed static assets, fonts |
serve cache, fall back to network |
| Network-first |
HTML/navigations, fresh-critical API |
try network, fall back to cache offline |
| Stale-while-revalidate |
API JSON, avatars, mixed content |
serve cache instantly, refresh in background |
| Network-only / Cache-only |
non-GET, analytics / pure offline assets |
bypass or only-cache |
- Never serve stale HTML forever — use network-first or SWR for documents so users discover updates.
- Only cache
GET. Don't cache responses to mutations.
- Version and namespace cache names; delete only this app's old versions on
activate. Cache Storage is shared across the origin, not isolated by service-worker scope.
- The Cache API does not enforce HTTP freshness or
Cache-Control: no-store. Explicitly exclude private/authenticated responses unless offline storage is designed for that data, partition by account when needed, and clear it on logout. Check response status before caching.
self.addEventListener('fetch', (e) => {
const { request } = e;
if (request.method !== 'GET') return;
if (request.mode === 'navigate') {
e.respondWith(fetch(request).catch(async () => {
const cache = await caches.open(VERSION);
return (await cache.match('/offline.html')) ??
new Response('Offline. Please reconnect and try again.', {
status: 503, headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
}));
}
});
Background sync & updates
- Background Sync (
sync event): queue eligible failed writes in IndexedDB with idempotency keys, bounded retries, and an explicit failure state. Feature-detect support and provide a foreground retry path. Periodic Background Sync has limited support.
- Tell users when a new SW is ready ("Refresh to update") rather than silently swapping, to avoid mid-session breakage.
Installable PWA
- Ship a web app manifest (
name, icons incl. maskable, start_url, display: standalone, theme_color).
- Install criteria vary by browser; serve over HTTPS with a suitable manifest. A service worker enables offline behavior but is not a universal install prerequisite.
beforeinstallprompt is not cross-browser: feature-detect it and provide platform-specific instructions where absent.
Use Workbox unless you have a reason not to
- Hand-written SWs get complex fast. Workbox gives battle-tested routing, strategies, precaching with revision manifests, and cleanup. Prefer it for non-trivial apps.
Footgun checklist
Reference
- MDN: Cache API, PWA installability, Service Worker API, Background Sync.
- web.dev "Learn PWA"; Chrome for Developers: Workbox + "Caching strategies overview".
1---2name: service-worker3description: Implements service-worker lifecycle, offline routing, cache ownership, update flows, and PWA behavior. Use for a worker or offline/PWA task; HTTP/CDN header policy alone belongs to caching.4license: MIT5---67# Service Worker & PWA89A service worker is a network proxy that runs in its own thread, independent of pages. It enables offline, fine-grained caching, push, and background sync — but it's powerful and easy to footgun (stale content, "won't update"). Treat versioning and updates as first-class.1011## Lifecycle (internalize this)1213`register → install → activate → (controls pages) → update`1415- **install**: precache the app shell. Call `self.skipWaiting()` to activate immediately (only if you handle updates gracefully).16- **activate**: clean up old caches. Call `self.clients.claim()` to control existing pages.17- A new SW **waits** until all old tabs close unless you `skipWaiting()`. This is why "my change didn't show up".18- Scope = the SW's directory and below. Serve it from the root to control the whole origin.1920```js21navigator.serviceWorker.register('/sw.js'); // page side2223// sw.js24const CACHE_PREFIX = 'my-app-shell-'; // unique to this app on the origin25const VERSION = `${CACHE_PREFIX}v3`;26const SHELL_ASSETS = ['/offline.html']; // include every offline fallback27self.addEventListener('install', (e) => {28 e.waitUntil(caches.open(VERSION).then((c) => c.addAll(SHELL_ASSETS)));29});30self.addEventListener('activate', (e) => {31 e.waitUntil(32 caches.keys().then((keys) =>33 Promise.all(keys.filter((k) => k.startsWith(CACHE_PREFIX) && k !== VERSION)34 .map((k) => caches.delete(k))),35 ),36 );37});38```3940## Caching strategies — match to content type4142| Strategy | Use for | Behavior |43|---|---|---|44| **Cache-first** | hashed static assets, fonts | serve cache, fall back to network |45| **Network-first** | HTML/navigations, fresh-critical API | try network, fall back to cache offline |46| **Stale-while-revalidate** | API JSON, avatars, mixed content | serve cache instantly, refresh in background |47| **Network-only / Cache-only** | non-GET, analytics / pure offline assets | bypass or only-cache |4849- **Never serve stale HTML forever** — use network-first or SWR for documents so users discover updates.50- Only cache `GET`. Don't cache responses to mutations.51- Version and namespace cache names; delete only this app's old versions on `activate`. Cache Storage is shared across the origin, not isolated by service-worker scope.52- The Cache API does not enforce HTTP freshness or `Cache-Control: no-store`. Explicitly exclude private/authenticated responses unless offline storage is designed for that data, partition by account when needed, and clear it on logout. Check response status before caching.5354```js55self.addEventListener('fetch', (e) => {56 const { request } = e;57 if (request.method !== 'GET') return;58 if (request.mode === 'navigate') {59 e.respondWith(fetch(request).catch(async () => {60 const cache = await caches.open(VERSION);61 return (await cache.match('/offline.html')) ??62 new Response('Offline. Please reconnect and try again.', {63 status: 503, headers: { 'Content-Type': 'text/plain; charset=utf-8' },64 });65 }));66 }67});68```6970## Background sync & updates7172- **Background Sync** (`sync` event): queue eligible failed writes in IndexedDB with idempotency keys, bounded retries, and an explicit failure state. Feature-detect support and provide a foreground retry path. Periodic Background Sync has limited support.73- Tell users when a new SW is ready ("Refresh to update") rather than silently swapping, to avoid mid-session breakage.7475## Installable PWA7677- Ship a **web app manifest** (`name`, `icons` incl. maskable, `start_url`, `display: standalone`, `theme_color`).78- Install criteria vary by browser; serve over HTTPS with a suitable manifest. A service worker enables offline behavior but is not a universal install prerequisite. `beforeinstallprompt` is not cross-browser: feature-detect it and provide platform-specific instructions where absent.7980## Use Workbox unless you have a reason not to8182- Hand-written SWs get complex fast. **Workbox** gives battle-tested routing, strategies, precaching with revision manifests, and cleanup. Prefer it for non-trivial apps.8384## Footgun checklist8586- [ ] Cache names versioned and app-scoped; other apps' caches preserved.87- [ ] HTML is network-first / SWR, not cache-first.88- [ ] Only `GET` cached; mutations always hit network.89- [ ] Update flow handled (skipWaiting + user prompt) so changes ship.90- [ ] Tested offline and on a slow network in DevTools → Application → Service Workers.9192## Reference9394- MDN: [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache), [PWA installability](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable), Service Worker API, Background Sync.95- web.dev "Learn PWA"; Chrome for Developers: Workbox + "Caching strategies overview".