Part IV β The Reach Β· Lesson 9.1
Service Workers
In one lineA background script that survives your page, proxies its requests, and updates on its own schedule.
Where you've seen itWriting a note in the metro, closing the tab, and finding it saved to the server by the time you're above ground.
Diagram
LIFECYCLE β and where teams get stuck
register()
β
βΌ
ββββββββββββ install event: precache the shell
βINSTALLINGβ β self.skipWaiting() to jump the queue
ββββββ¬ββββββ
β old SW still controlling the page?
βΌ
ββββββββββββ the new SW WAITS here until every tab
β WAITING β using the old one is closed
ββββββ¬ββββββ β "I deployed but users still see the old app"
β
βΌ
ββββββββββββ activate event: delete old caches
β ACTIVATEDβ β clients.claim() to control open pages now
ββββββ¬ββββββ
β
βΌ
ββββββββββββ intercepts fetch for every page in scope
β CONTROL β
ββββββββββββ
A reload is NOT enough to activate a new SW β the old page
must be fully closed, or you must skipWaiting deliberately.SCOPE AND ISOLATION
/sw.js at the root βββΊ controls the whole origin
/app/sw.js βββΊ controls /app/* only
The SW runs on its OWN thread:
β has fetch, Cache Storage, IndexedDB, postMessage
β has NO DOM, no window, no localStorage (sync APIs banned)THE WRITE QUEUE β what makes "offline" actually work
user edits offline
β
βΌ
write to IndexedDB (6.5) βββΊ UI reads from here, instantly
β
βΌ
append to "outbox" store
β
β connection returns (or Background Sync fires)
βΌ
replay outbox in order βββΊ server
β
ββ β ack βββΊ remove from outbox, update local record
ββ β conflict βββΊ resolve: last-write-wins, merge, or askHow it works
- Register after load so the service worker never competes with first paint.
- Precache the shell on
install; delete old caches onactivate, keyed by a version string. - Decide the update policy: either
skipWaiting+clients.claim(fast, risks a version mismatch between open tabs and new assets), or show a "New version available β reload" prompt. Pick deliberately. - Serve reads from cache/IndexedDB, using the strategies in 6.8.
- Queue writes in IndexedDB and replay them on reconnect, ideally via the Background Sync API where available.
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE_V).map((k) => caches.delete(k)))
).then(() => self.clients.claim())
);
});Trade-offs
| Gain | Cost |
|---|---|
| Works offline, instant repeat loads | A bad SW is sticky on users' devices |
| Background sync and push | Debugging is harder β separate thread |
| Full control over caching | Version-skew bugs between tabs |
Always ship a kill switch. A service worker that unregisters itself and clears caches is your only remedy if you deploy a broken one β you cannot reach those devices any other way.
Interview angle
"You deployed a fix but users still see the broken version. Why?"
- The old service worker keeps control until every tab using it closes; a refresh doesn't replace it.
- Either call
skipWaitingandclients.claim, or surface an explicit update prompt β silence is what causes this. - It's also why a kill-switch SW and cache versioning are mandatory before your first production deploy.
Recap
- Install β waiting β activated β controlling; the waiting step is where deploys appear stuck.
- No DOM, no sync storage β the SW lives on its own thread with fetch, Cache Storage, and IndexedDB.
- Offline writes need a queue plus a conflict rule, not just a cache.