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:
- Installing (
installevent fires). The worker pre-caches resources viaevent.waitUntil(cache.addAll([...])). - Installed / waiting — install succeeded. The worker is held waiting if a previous active worker still controls clients.
- Activating (
activateevent fires). The worker takes control when no clients are bound to a previous worker, or whenself.skipWaiting()was called.activateis the right place to prune old caches. - 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
pushevent 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.Responsebodies are consumed; storing the original then returning it to the page throws on the second read. - Skipping the waiting phase without informing the user.
skipWaitingmay break in-flight pages; gate it on a user prompt for non-trivial apps. - Caching
POSTresponses. 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 explicitregistration.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.