The HTTP cache decides how long the browser may reuse a file, but the rules are set by the server and are the same for everyone. A service worker offers much finer control: it is a script that sits between the page and the network, intercepts every request and decides in code whether to respond from the cache, from the network or from a combination of the two. With it, a site can load almost instantly on repeat visits and keep working when the connection drops.
How it sits between page and network
The service worker is registered from the page:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
From then on, for all pages within its scope (by default the folder containing the file and its subfolders), every request goes through the worker's fetch event. Files are stored with the Cache Storage API, a store separate from the HTTP cache and managed entirely by code.
Two requirements to remember: service workers only work over HTTPS (with the exception of localhost during development) and they have no access to the DOM.
The main strategies
Each type of resource deserves a different strategy.
Cache first
The worker looks in the cache first and goes to the network only if the file is missing. It is ideal for versioned assets, such as app.3f9a1c.js or fonts, whose name changes with every modification: once cached they never need to be checked again.
self.addEventListener('fetch', (event) => {
if (event.request.destination === 'font') {
event.respondWith(
caches.match(event.request).then(r => r || fetch(event.request))
);
}
});
Network first
The worker tries the network and, if it fails or is too slow, falls back to the cache. It suits page HTML and data that must be fresh, with the cache as a safety net for offline use. Adding a timeout is wise: a weak connection that does not respond is worse than a slightly stale page.
Stale-while-revalidate
The worker responds immediately with the cached copy and, in parallel, downloads an updated version for next time. The user gets instant speed and content updates one visit later. It works well for avatars, non-critical images, configuration files and API responses that rarely change.
| Resource | Strategy | Reason |
|---|---|---|
| JS and CSS with a hash in the name | Cache first | The name changes with every version |
| Fonts | Cache first | They rarely change |
| Page HTML | Network first with timeout | Fresh content, offline fallback |
| Content images | Stale-while-revalidate | Speed with background refresh |
| Payment or login requests | Network only | Never serve stale responses |
The lifecycle and updates
The part that causes the most trouble is not the strategies but the updates. A service worker goes through a few phases:
- install: the new worker is downloaded and can pre-cache the essential files;
- waiting: if a previous worker still controls open tabs, the new one waits;
- activate: when the old tabs are closed, the new one takes control, and this is the right moment to delete caches from previous versions.
const VERSION = 'v7';
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys => Promise.all(
keys.filter(k => k !== VERSION).map(k => caches.delete(k))
))
);
});
The waiting phase explains a common complaint: "I shipped a fix but users still see the old version". You can force the switch with skipWaiting(), but only if the new version is compatible with pages already open; otherwise it is better to show an "An update is available" notice with a button to reload.
The sw.js file itself should not be cached for long by the HTTP cache: browsers recheck it periodically anyway, but serving it with a short lifetime avoids delays in updates.
Navigation preload
With a network-first strategy for HTML there is a hidden cost: on each navigation the browser must first start the service worker, if it was idle, and only then does the network request begin. Navigation preload removes this wait: the request starts in parallel with the worker's startup, and the worker then uses the response already on its way.
self.addEventListener('activate', (e) => {
e.waitUntil(self.registration.navigationPreload?.enable());
});
Using a library
Writing strategies, expiry, storage limits and clean-up by hand is possible, but repetitive and error-prone. Workbox is the most widely used library for this: it provides ready-made strategies, entry expiry management and integration with the main build tools to generate the list of files to pre-cache.
Verifying
In Chrome DevTools, in the Application panel, the Service workers section shows the worker's status, lets you force an update and simulate being offline; the Cache Storage section lists the stored files. In the Network panel, responses served by the worker are labelled as such in the size column.
In short
- A service worker intercepts requests and decides in code where to respond from.
- Cache first for versioned assets, network first for HTML, stale-while-revalidate for secondary content.
- Manage the lifecycle: delete old caches in
activateand communicate updates. - Enable navigation preload so navigations are not slowed down.
- For real projects, a library such as Workbox avoids many mistakes.