What are service workers, and how do they enable PWA features?
A service worker is a background JS thread that intercepts network requests for an origin. Pair with the Cache API to serve responses offline, push notifications, and background sync. Lifecycle: install → activate → fetch.
A service worker is a JS file the browser runs in a separate thread, scoped to an origin, that can intercept and respond to network requests. It's the engine behind Progressive Web Apps: offline support, push notifications, install-to-home-screen, background sync.
Lifecycle (interviewers love this).
- Register.
navigator.serviceWorker.register("/sw.js", { scope: "/" })from the page. Scope determines which paths the SW controls. Default scope is the location of the SW file — keep it at the root for site-wide control. - Install.
self.addEventListener("install", e => e.waitUntil(precache())). Pre-cache critical assets. Promise resolved → installed. - Activate. Old SW (if any) is being replaced. Clean up old caches here. The new SW does not control existing pages until they reload, unless you call
self.skipWaiting()(in install) andclients.claim()(in activate). - Fetch. Every navigation and asset request fires
fetchon the SW. You can respond with cache, network, or a synthesized response.
The Cache API. A key-value store of Request → Response, scoped to the origin and named by you. Independent of HTTP cache. Survives reloads.
Caching strategies (Workbox names them).
- Cache-first. Look in cache; if hit, return it; else fetch and cache. Best for hashed static assets.
- Network-first. Fetch; on failure, fall back to cache. Best for HTML / API where freshness matters.
- Stale-while-revalidate. Return cache immediately, fetch in background, update cache. Best for "fast and eventually fresh" — most app shell content.
- Network-only / Cache-only. Self-explanatory. Useful for analytics (network-only) or offline-only assets (cache-only).
// sw.js — stale-while-revalidate
self.addEventListener("fetch", (event) => {
if (event.request.method !== "GET") return;
event.respondWith((async () => {
const cache = await caches.open("v1");
const cached = await cache.match(event.request);
const networkPromise = fetch(event.request).then(resp => {
if (resp.ok) cache.put(event.request, resp.clone());
return resp;
}).catch(() => undefined);
return cached ?? (await networkPromise) ?? new Response("Offline", { status: 503 });
})());
});PWA features it unlocks.
- Offline. Pre-cache the app shell; fall back to a cached offline page for navigations.
- Install to home screen. Web App Manifest (
manifest.json) + HTTPS + a registered SW = installable. Browser shows the install prompt. - Push notifications. SW listens for
pushevents and shows notifications even when the page is closed. - Background sync.
registration.sync.register("upload")retries network work when the user comes back online (Chromium only at the moment). - Periodic background sync (Chromium): refresh data periodically.
Critical gotchas.
- HTTPS only (except localhost). Service workers can hijack requests, so they require a secure context.
- Updates are sticky. A new SW waits until all clients close the old one. Use
skipWaiting+clients.claimcarefully — can update mid-session and break. - Cache versioning is your job. When you change the SW, bump the cache name (
v1→v2) and delete old caches inactivate— otherwise old assets persist forever. fetchruns for every request. Heavy logic in there can slow page loads. Keep handlers lean.- No DOM access. SW is a Worker — no
window, nodocument. Communicate with pages viapostMessageandMessageChannel. - Range requests / video behave specially — pass through to network; don't try to cache.
Use Workbox in production. Google's library wraps all the strategies, cache versioning, and precaching. Don't hand-write SWs unless you need exotic behavior.
When NOT to use a SW. Static marketing sites that the HTTP cache already handles. Anything where install bugs (sticky old caches) would be catastrophic for low-engagement users. Always have an "unregister" escape hatch.
Code
Follow-up questions
- •How do you handle SW updates without breaking a logged-in user mid-session?
- •When would you choose stale-while-revalidate over network-first?
- •How would you debug a 'won't update' SW issue?
- •What does the Web App Manifest contribute beyond the SW?
Common mistakes
- •Forgetting to bump the cache name on deploy — old assets persist.
- •Calling skipWaiting + clients.claim without thought — mid-session asset swap can break the page.
- •Trying to cache POST or non-GET requests — Cache API only handles GET.
- •Putting heavy logic in the fetch handler — slows every request.
Performance considerations
- •Pre-cache only the shell (HTML/CSS/JS); lazy-cache the rest.
- •Stale-while-revalidate is the highest-perceived-perf strategy for repeat visits.
Edge cases
- •Range requests (video) — bypass the SW.
- •Auth flows that redirect — careful caching the HTML can break SSO.
- •Browser private mode disables SW persistence.
Real-world examples
- •Twitter Lite, Pinterest, Starbucks PWA, Notion's offline mode — all SW + Cache API.