"The service worker is a process that wakes up, does one thing, and goes back to sleep. Build your extension assuming it's mostly asleep, and the whole MV3 design clicks into place."
The Background, Reimagined
MV2's background page was a long-lived HTML document with a JavaScript runtime that stayed in memory for as long as Chrome was running. Convenient — you could stash state on globals, hold WebSocket connections open, run timers. Expensive — every installed extension cost memory whether you used it or not. And a security weak point: if a long-lived process gets compromised, it stays compromised until Chrome restarts.
MV3 swapped the persistent page for a service worker: an event-driven JavaScript context that Chrome spins up on demand and tears down when idle. The same concept that powers PWAs, repurposed as the background context for extensions. Lighter, harder to abuse, and forces you to think about state explicitly instead of leaning on memory.
Anatomy of a Service Worker
A service worker is a single JavaScript file (no HTML, no DOM, no window). It runs in its own thread, isolated from popups and content scripts. The top of the file runs on every wake-up; listeners registered there receive events.
You can't use window or document. localStorage isn't available. fetch works. self is the global. The chrome.* APIs work the same as in popups, with one twist: anything async must complete (or return true from a listener) before the worker can be evicted.
The Service Worker Lifecycle
Five states matter:
- Installing — Chrome registers the worker; the top of the file runs once.
- Activating — registration succeeds;
chrome.runtime.onInstalledfires. - Running — an event woke the worker; it's actively executing.
- Idle — no events firing, no in-flight async; Chrome's clock is ticking toward eviction.
- Evicted — Chrome shut the worker down. Module-level state gone. Listeners still registered (they live in Chrome's record of the worker, not in worker memory).
The next event — any registered listener firing — wakes the worker cold. Chrome re-runs the top of the file (this is critical: listener re-registration happens by re-executing chrome.runtime.onMessage.addListener(...) at the top) and then dispatches the event.
Why Idle Eviction Matters
Three reasons:
- Memory — at any moment, the average user has zero running service workers across their installed extensions. Memory only spikes when extensions are actively doing work.
- Security — a compromised worker can't sit there forever waiting to exfiltrate data; it gets evicted, re-spawned cold, and Chrome's permissions checks run again.
- Performance — Chrome's renderer process isn't bloated by always-on extension code.
The cost is a mental shift: you can't trust in-memory state across events. Anything important goes to chrome.storage. Anything ephemeral is acceptable to lose.
What ClipDeck Will Do With It
Through Track 2, ClipDeck grows its first non-UI surface. Lesson 2 registers background.js via the manifest. Lesson 3 dives deeper into the event-driven lifecycle. Lesson 4 introduces chrome.storage.local as the state layer. Lesson 5 wires popup ↔ worker message passing. Lesson 6 ties it together: chrome.tabs.onUpdated increments a visit counter in storage, and the popup shows the running total.
By the end of Track 2, ClipDeck is no longer pure UI — it's observing the browser in the background and accumulating state. The R in CRUD warms up.
embeds/chrome/background.js, it surprised me how small it is — 4 KB total. Per-tab context cache as a Map (acceptable because Chrome usually wakes the worker before eviction matters for hot tabs), forward latest payload to side panel, expose a request-context handler. That's it. The brain stayed upstream in cwkPippa. The worker is just plumbing — and that's the right shape for every MV3 background script.