Skip to content

Navigation preload: hiding service worker boot time from navigations

In one line: Navigation preload lets the browser start fetching a navigation request in parallel with service worker boot-up, so a worker that has to cold-boot is no worse off than one that is already running.

When a user navigates to a page controlled by a service worker, the browser must boot the service worker (if it is not already running), send it a fetch event, and wait for the result. A service worker cannot process events until it has finished booting, and when a worker does need to boot from scratch, that startup delay can slow down the response to a navigation request — even though the eventual response might come from a cache, which is normally very fast.

Navigation preload is enabled with NavigationPreloadManager.enable(), typically in the service worker’s activate event handler, with feature detection via ServiceWorkerRegistration.navigationPreload:

addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
if (self.registration.navigationPreload) {
await self.registration.navigationPreload.enable();
}
})(),
);
});

self.registration.navigationPreload returns the NavigationPreloadManager object if supported, or undefined otherwise.

Method Purpose
enable() Enables navigation preloading; returns a Promise resolving to undefined.
disable() Disables navigation preloading; returns a Promise resolving to undefined.
setHeaderValue() Sets the value of the Service-Worker-Navigation-Preload HTTP header sent with preload requests; returns an empty Promise.
getState() Returns a Promise resolving to an object indicating whether preloading is enabled and what header value is used.

Navigation preload must be used together with a fetch event handler: the preloaded result is read from FetchEvent.preloadResponse, a Promise that resolves to a Response (or undefined if preload did not run for that request):

addEventListener("fetch", (event) => {
event.respondWith(
(async () => {
const cachedResponse = await caches.match(event.request);
if (cachedResponse) return cachedResponse;
const response = await event.preloadResponse;
if (response) return response;
return fetch(event.request);
})(),
);
});

preloadResponse resolves to a Response only when navigation preload is enabled for the service worker, the request is a GET request, and the request is a navigation request; otherwise it resolves to undefined. It is only available inside service workers.

The Service-Worker-Navigation-Preload header

Section titled “The Service-Worker-Navigation-Preload header”

The browser automatically sends the Service-Worker-Navigation-Preload header (default value true) with preload requests, letting a server distinguish preload requests from normal ones. setHeaderValue() can change this value, for example to a cache-version ID:

navigator.serviceWorker.ready
.then((registration) =>
registration.navigationPreload.setHeaderValue(newValue),
)
.then(() => {
console.log("Done!");
});

If the normal and preload responses can differ, the server must set Vary: Service-Worker-Navigation-Preload so caching behaves correctly.

NavigationPreloadManager and FetchEvent.preloadResponse require a secure context (HTTPS) and are Baseline widely available — well established and supported across many devices and browsers since April 2022.

  • Enable navigation preload in the activate handler, guarded by feature detection.
  • Await event.preloadResponse inside the fetch handler; do not use the response elsewhere.
  • Keep a preload request alive with event.waitUntil() if you end up not using its response (for example, after a cache hit).
  • Set Vary: Service-Worker-Navigation-Preload on the server if preload and normal responses can differ.