C.W.K.
Stream
Lesson 01 of 06 · published

What Is a Service Worker, Really?

~10 min · service-worker, background, mv3, lifecycle, concepts

Level 0Extension Curious
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"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.onInstalled fires.
  • 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.

Design every service worker as if it just woke up. Read state from storage, do the work, write state back, expect to be evicted any second. The pattern is short, focused, and stateless at the module level.
When I first read cwkPippa's 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.

Code

Minimal MV3 service worker — listeners only, no module-level state·javascript
// background.js — minimum viable service worker.
// Runs at the top on every wake-up.

console.log("[ClipDeck SW] alive at", new Date().toISOString());

chrome.runtime.onInstalled.addListener((details) => {
  console.log("[ClipDeck SW] onInstalled:", details.reason);
});

chrome.runtime.onStartup.addListener(() => {
  console.log("[ClipDeck SW] onStartup — Chrome launched");
});

// Notice: no module-level state. No setInterval. No fetch loops.
// Every event handler is the entire unit of work.
MV3 service worker lifecycle — visual·text
[install / first load]
     │
     ▼
  [activating] ──onInstalled fires──┐
     │                              │
     ▼                              ▼
  [running] ◀────event arrives──── [idle, ~30s]
     │                              │
     │                              ▼
     └───────────────────────── [evicted]
                                    │
                                    ▼
                            (next event wakes cold)

External links

Exercise

Open chrome://serviceworker-internals/?devtools in a new tab — this is Chrome's deep view into every registered service worker on the machine (extensions plus PWAs). Scroll or Ctrl+F for "ClipDeck". Track 1's ClipDeck has no background.service_worker declared, so it should NOT appear in the list. Lesson 2 changes that. While you're here, find one or two PWAs you've installed (Notion, Spotify, Google Docs) and observe their service worker entries — Status (RUNNING / STOPPED), URL, registration ID. Write down which extensions or PWAs currently have RUNNING workers vs STOPPED.
Hint
chrome://serviceworker-internals is more verbose than the public-facing UI; it shows every state transition. If ClipDeck appears there before Lesson 2, you accidentally added background.service_worker somewhere — check your manifest. Stopped workers in the list are normal — that's the idle-eviction behavior we just described.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.