⚡ Web performance

Service workers and caching strategies: speed and offline support

Cache first, network first and stale-while-revalidate explained with examples, plus the service worker lifecycle and the mistakes that block updates.

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:

  1. install: the new worker is downloaded and can pre-cache the essential files;
  2. waiting: if a previous worker still controls open tabs, the new one waits;
  3. 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.

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 activate and communicate updates.
  • Enable navigation preload so navigations are not slowed down.
  • For real projects, a library such as Workbox avoids many mistakes.
MM
Marco Marzaduri

Marco Marzaduri works in cybersecurity, digital forensics and web development. A computer science graduate, he combines rigorous technical analysis with attention to the people who use technology.

Read also