Home JS

Service workers: registration, lifecycle, fetch handling

A service worker is a programmable network proxy controlled by the page's origin. It enables offline, background sync, and push, but introduces a lifecycle that can outlive the page.

What a service worker is

A service worker is a JavaScript file the browser runs in a background context, separate from any page. The browser routes network requests from pages within its scope through the worker’s fetch event handler, allowing the worker to respond from a cache, modify the request, or pass it through.

Service workers run on HTTPS only (with localhost exempted for development). They have no DOM, no window, no synchronous storage, and no direct access to a page’s variables. Communication with pages is via postMessage and the Clients API.

Registration

Pages register a worker by URL and scope:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js', { scope: '/' });
}

The default scope is the path the worker file is served from. A worker at /sw.js defaults to scope /. Serving the worker file with the Service-Worker-Allowed: / response header lets the registration declare a broader scope.

Each origin may register multiple workers, but only one can be active for a given scope at a time.

Lifecycle states

A worker passes through four states:

  1. Installing (install event fires). The worker pre-caches resources via event.waitUntil(cache.addAll([...])).
  2. Installed / waiting — install succeeded. The worker is held waiting if a previous active worker still controls clients.
  3. Activating (activate event fires). The worker takes control when no clients are bound to a previous worker, or when self.skipWaiting() was called. activate is the right place to prune old caches.
  4. Activated — handles fetch, push, sync, message.

self.skipWaiting() plus clients.claim() lets a new worker take over without waiting for users to close every tab. The trade-off is that pages may receive a different worker than the one they loaded with.

The fetch event

self.addEventListener('fetch', (event) => {
  if (event.request.method !== 'GET') return;
  event.respondWith(
    caches.match(event.request).then((cached) =>
      cached || fetch(event.request)
    )
  );
});

Calling event.respondWith(promise) commits the worker to providing the response. Not calling it lets the request proceed normally.

Several caching strategies are common:

  • Cache-first — return from cache; fetch only on miss.
  • Network-first — try the network; fall back to cache on failure (good for HTML).
  • Stale-while-revalidate — return cached immediately; fetch in the background to update the cache.

The Cache API stores Request / Response pairs. It is not a key-value store; mismatched headers (e.g. cookies in the request) can cause cache misses. Use cache.addAll([url1, url2]) for static preloads and cache.put(req, res.clone()) for dynamic results.

Background sync, periodic sync, and push

  • Background sync queues a network operation while the user is offline and replays it when connectivity returns.
  • Periodic background sync lets the browser wake the worker to refresh data on a schedule.
  • Push delivers messages from a server via the Push API; the worker handles a push event and shows a notification.

Each of these is gated by user permission and platform support; treat them as enhancements rather than required behaviour.

Browser engine support

  • Core service workers, install/activate, fetch handling — interop across Chromium (Blink), WebKit, and Gecko since 2018.
  • Cache API — interop since 2018.
  • Background fetch — Chromium (Blink) only as of 2025.
  • Background sync — Chromium (Blink) only.
  • Periodic background sync — Chromium (Blink) only, gated by installed-PWA status.
  • Push — interop across Chromium (Blink), WebKit, and Gecko since 2023, with platform-specific delivery semantics.
  • Navigation preload — interop across Chromium (Blink), WebKit, and Gecko since 2022.

Common pitfalls

  • Caching the HTML and the JS bundle separately. A new HTML loads the old JS chunks. Either version-stamp filenames and cache them as a single unit, or use network-first for HTML.
  • Forgetting to clone() the response. Response bodies are consumed; storing the original then returning it to the page throws on the second read.
  • Skipping the waiting phase without informing the user. skipWaiting may break in-flight pages; gate it on a user prompt for non-trivial apps.
  • Caching POST responses. The Cache API only stores GET by default; explicit handling is needed for other methods.
  • update() not called. Browsers update workers on navigation, but apps that stay open for hours may want explicit registration.update() polling.

Further reading

  • W3C Service Workers 1 spec (CR 2022).
  • The PWA Builder reference tracks platform-specific install and push semantics that the spec leaves open.
  • Jake Archibald’s Offline Cookbook (2014, updated 2022) is the canonical caching-strategy reference; about 70% of installed PWAs use one of the four named strategies.
  • The Push API (w3.org/TR/push-api) reached interop in 2023; iOS 16.4 added support, completing the cross-engine baseline.
  • Workbox 7 (2023) automates the cache-strategy pattern and is the most-used wrapper, though direct service-worker code is closer to the spec.