FSD Frontend System Design

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.

  • Time18 min
  • DiagramLifecycle states + the update trap
  • Chapter9 Β· Offline Support

Diagram

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.
diagram
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)
diagram
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 ask

How it works

  1. Register after load so the service worker never competes with first paint.
  2. Precache the shell on install; delete old caches on activate, keyed by a version string.
  3. 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.
  4. Serve reads from cache/IndexedDB, using the strategies in 6.8.
  5. Queue writes in IndexedDB and replay them on reconnect, ideally via the Background Sync API where available.
js
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

GainCost
Works offline, instant repeat loadsA bad SW is sticky on users' devices
Background sync and pushDebugging is harder β€” separate thread
Full control over cachingVersion-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 skipWaiting and clients.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.