Skip to content

Service workers: what they are, and a minimal one

Published Updated

A service worker is a script the browser runs in its own worker thread, separate from any page, which can intercept network requests from the pages it controls and answer them itself. That is what makes offline support, instant repeat loads, and push possible: the worker keeps running after the tab is gone.

It has no DOM access, and it only runs in a secure context (HTTPS, or localhost during development).

Register it from the page:

if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
} else {
// No service worker: the site still works, it just has no offline layer.
}

And in /sw.js, answer requests from a cache, falling back to the network:

self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((hit) => hit || fetch(event.request)),
);
});

That is a complete, working service worker. Everything else — precaching, update strategy, expiry — is refinement on top of these two pieces.

A registered worker is installed, then activated, and only then does it start controlling pages. A page loaded before the worker activated stays uncontrolled until it is reloaded. When you ship a new sw.js, the browser installs it alongside the old one and leaves it waiting until the old worker releases its clients — which is why a deploy does not take effect on the very next reload unless you ask it to.

See Service worker lifecycle and The update flow and skipWaiting.

  • Scope — a worker at /js/sw.js controls only /js/. Serve it from the root, or send Service-Worker-Allowed. See Registration and scope.
  • Stale users after a deploy — the new worker sits waiting behind the old one.
  • Caching everything reflexively — a cache-first strategy on HTML strands users on an old page. Choose per request type: Caching strategies.
  • Debugging it like page code — it has its own lifecycle and its own DevTools pane. See Debugging service workers.

← Back to the Reference overview.