Service Workers — what they do and how to use them
A service worker is a background script that proxies network requests for its scope. Use cases: offline support, asset caching for instant repeat loads, background sync of queued mutations, push notifications. Lifecycle: install → waiting → activate → fetch interception. Update gotcha: the new SW activates only after old tabs close — handle with skipWaiting + a 'reload to update' UX.
A service worker is a background JS thread that sits between your app and the network for its scope. It can intercept fetches, cache responses, and respond when offline. It's the engine behind PWAs.
1. Registration
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js", { scope: "/" });
}- Must be HTTPS (or localhost).
- Scope = the URL prefix it controls.
- One registered SW per scope.
2. Lifecycle
- Install —
installevent; precache assets. - Waiting — if a previous SW controls clients, the new one waits.
- Activate —
activateevent; clean up old caches. - Fetch —
fetchevents for requests in scope.
// sw.js
const CACHE = "app-v3";
self.addEventListener("install", (e) => {
e.waitUntil(caches.open(CACHE).then((c) => c.addAll(["/", "/app.js", "/app.css"])));
});
self.addEventListener("activate", (e) => {
e.waitUntil(
caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
);
});
self.addEventListener("fetch", (e) => {
e.respondWith(
caches.match(e.request).then((cached) => cached || fetch(e.request))
);
});3. Caching strategies
- Cache-first — return cache; fallback to network. For hashed immutable assets.
- Network-first — try network; fallback to cache. For HTML/API where freshness matters.
- Stale-while-revalidate — return cache immediately; update in background. Best for "fresh-ish" assets.
- Network-only — auth, real-time.
- Cache-only — for prefetched content.
Use Workbox instead of hand-rolling.
4. Update model — the gotcha
A new SW activates only after all controlled tabs close. In a SPA where the user keeps a tab open for days, the new SW sits in "waiting" forever.
Solutions:
self.skipWaiting()in install → activates immediately on next page load.- Detect
controllerchangeand offer a "Reload to update" prompt:
navigator.serviceWorker.addEventListener("controllerchange", () => {
// show "update available" toast → user clicks → location.reload()
});- Versioned cache names so old caches are cleaned up in
activate.
5. Capabilities beyond fetch
- Push notifications —
pushevent; show via Notification API. - Background Sync — queue mutations made offline;
syncevent when network returns. - Periodic Background Sync — limited support; periodic refresh.
6. What it can't do
- No DOM access — it's a worker.
- Communicates via postMessage or by intercepting fetches.
- Wakes only briefly — long-running tasks are killed.
- No localStorage — use IndexedDB or Cache Storage.
7. Debugging
- Chrome DevTools → Application → Service Workers.
Update on reloadcheckbox is your friend in dev.- Always have an unregister kill-switch — a stale SW can break your site for everyone until users clear it.
8. Real risks
- A broken SW caches a broken bundle → all return users see the broken page until they clear storage.
- Cache for API responses without thinking → users see ancient data.
- Always test the update flow before shipping.
Interview framing
"A service worker is a background JS thread that proxies fetches for its scope. The big use cases are offline support (cache + serve when network fails), instant repeat loads (cache the app shell), background sync (queue mutations made offline), and push notifications. The lifecycle is install → waiting → activate → fetch. The biggest gotcha is the update model — a new SW waits until old tabs close, so you need skipWaiting plus a 'reload to update' UX. I'd reach for Workbox rather than hand-roll. And always ship a kill-switch — a broken SW can keep serving a broken bundle to users forever."
Follow-up questions
- •Walk through the SW lifecycle.
- •Why does the new SW not activate immediately?
- •When would you use stale-while-revalidate vs cache-first?
- •What's a SW kill-switch and why have one?
Common mistakes
- •Caching API responses with cache-first → stale data forever.
- •No skipWaiting + no update UX → users stuck on old version.
- •Forgetting to version and clean up old caches.
- •No kill-switch for a bad SW deploy.
Performance considerations
- •Cached shell makes repeat loads near-instant. SW startup adds a tiny first-request cost. Watch cache size; clean up old caches; precache only what's hot.
Edge cases
- •Bad SW shipped — needs a kill-switch route.
- •Cache quota exceeded.
- •Multiple tabs across versions.
- •SW running while user logs out.
Real-world examples
- •Twitter Lite, Pinterest, Spotify web player.
- •Workbox-powered Next.js / CRA PWAs.