Skip to content

Service Worker · Concept

Workbox

Published

Workbox is a set of JavaScript libraries, maintained by the Chrome team, that implement the routing, caching, precaching, and update-messaging code a service worker otherwise repeats by hand; the current major version is 7 (released 2023-04). It runs on the standard Service Worker, Cache, and Fetch APIs only, so it works in every browser with a service worker (Chrome 40, Firefox 44, Safari 11.1, Edge 17; BCD api.ServiceWorker), and its optional modules degrade where their underlying API is missing, such as workbox-background-sync in Firefox and Safari.

Each workbox-* package maps to one service worker task; the worker script imports only what it uses, either from npm through a bundler or as ES modules from a CDN.

Package What it replaces in a hand-written worker
workbox-routing The if/else chain in the fetch handler: registerRoute(matcher, handler) and NavigationRoute
workbox-strategies The five strategy bodies: CacheFirst, NetworkFirst, StaleWhileRevalidate, NetworkOnly, CacheOnly, with networkTimeoutSeconds and plugin hooks
workbox-precaching The install/activate cache fill and cleanup, keyed by content revision
workbox-expiration Cache size and age limits (maxEntries, maxAgeSeconds)
workbox-cacheable-response The response.ok check before cache.put()
workbox-background-sync An IndexedDB queue replayed on the sync event; in browsers without Background Sync the queue is replayed whenever the worker next starts
workbox-broadcast-update A BroadcastChannel message when a Stale-While-Revalidate refresh changes a cached response
workbox-window Page-side registration, update detection, and skipWaiting messaging

Precaching is the part that needs a build step. A build tool scans the output directory and emits an array of { url, revision } entries into the worker in place of the self.__WB_MANIFEST token; precacheAndRoute() stores each file in a cache named workbox-precache-v2-<scope>, keyed by URL with the revision appended as a __WB_REVISION__ query parameter, and installs a Cache First route for those URLs. On update, only entries whose revision changed are fetched; the activate handler deletes entries no longer in the manifest. Files whose name already contains a content hash get revision: null so the URL alone identifies the version.

Two build modes produce that manifest. GenerateSW (in workbox-build, workbox-cli, workbox-webpack-plugin, and vite-plugin-pwa) writes the complete worker from configuration and is sufficient when the worker needs nothing beyond precaching and runtime caching rules; InjectManifest takes a worker source file you write, replaces the __WB_MANIFEST token, and is required for any custom fetch, push, sync, or message handler. The trade-off is that GenerateSW regenerates the worker on every build so configuration is the only source of truth, while InjectManifest makes the worker ordinary code at the cost of maintaining the imports yourself.

On the page, workbox-window’s Workbox class wraps register() and raises installed, waiting, controlling, and activated events; wb.messageSkipWaiting() posts a { type: 'SKIP_WAITING' } message; a GenerateSW worker includes the matching message listener, while an InjectManifest worker must add the four-line listener shown in the first example.

The first example is a complete InjectManifest worker with precaching and one runtime route; the second is the page side, including the branch for a browser without service workers.

An InjectManifest worker with precaching and an image route

Section titled “An InjectManifest worker with precaching and an image route”

The __WB_MANIFEST token is replaced at build time; the ExpirationPlugin bounds the runtime cache, which precaching does not need because the manifest already bounds it.

// src/sw.js: the swSrc file; the build tool fills __WB_MANIFEST
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
cleanupOutdatedCaches(); // removes caches from older precache formats
precacheAndRoute(self.__WB_MANIFEST); // [{ url: '/index.html', revision: 'a1b2' }, …]
self.addEventListener('message', (event) => {
if (event.data?.type === 'SKIP_WAITING') self.skipWaiting(); // paired with workbox-window
});
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60 }),
],
})
);

Including 0 in statuses caches opaque cross-origin images, which Chromium pads to about 7 MB each in quota accounting; omit it when the images are same-origin or served with CORS so that only real 200 responses are stored.

Registering with workbox-window and detecting support

Section titled “Registering with workbox-window and detecting support”

Workbox.register() resolves to the registration or rejects with the same errors as navigator.serviceWorker.register(). When serviceWorker is absent the page keeps working online-only, so the import is placed behind the feature test to avoid loading the module for nothing.

page.js
if ('serviceWorker' in navigator) {
const { Workbox } = await import('workbox-window');
const wb = new Workbox('/sw.js');
wb.addEventListener('waiting', () => {
showBanner('A new version is ready.', () => {
wb.addEventListener('controlling', () => window.location.reload());
wb.messageSkipWaiting();
});
});
wb.register();
} else {
// No service worker: skip the update banner; the app runs from the network.
}

Reloading inside controlling rather than immediately after messageSkipWaiting() waits for the new worker to take over, so the reloaded page is served by the new precache; reloading sooner can load a mix of old HTML and new assets.

Specifications

SpecificationStatus
None.