Skip to content

Service Worker · API

NavigationPreloadManager

Published

NavigationPreloadManager, reached as registration.navigationPreload, tells the browser to start the network request for a navigation at the same time as it starts the service worker, instead of waiting for the worker to boot and call fetch(); the worker then reads the early response from FetchEvent.preloadResponse. It is available in Chrome 59, Edge 79, Firefox 99, and Safari 15.4 (BCD api.NavigationPreloadManager), and only in secure contexts.

// Service worker (or page, via navigator.serviceWorker.ready)
await registration.navigationPreload.enable();
await registration.navigationPreload.disable();
await registration.navigationPreload.setHeaderValue(value);
const state = await registration.navigationPreload.getState();
// Service worker, inside the fetch handler
const preloaded = await event.preloadResponse; // Response or undefined

Preload is a per-registration setting that persists across worker restarts and updates, so it is enabled once, typically in activate, and does not need to be re-enabled on every start.

The manager has four methods, all returning promises; the state object has two fields.

Member Returns Behaviour
enable() Promise<void> Turns preload on for the registration. Subsequent navigations inside the scope get a parallel GET with the Service-Worker-Navigation-Preload request header.
disable() Promise<void> Turns preload off. event.preloadResponse then resolves with undefined for every navigation.
setHeaderValue(value) Promise<void> Sets the value of the Service-Worker-Navigation-Preload header sent with preload requests. The default is true.
getState() Promise<{ enabled: boolean, headerValue: string }> Reports whether preload is on and the current header value.
FetchEvent.preloadResponse Promise<Response | undefined> The preload response for a navigation GET while preload is enabled; undefined for subresources, non-GET requests, and when preload is off.

The header exists so a server can return a different payload for a preload request (for example, JSON data instead of a full HTML page when the worker renders the shell itself). A server that varies on it must send Vary: Service-Worker-Navigation-Preload, otherwise an intermediate HTTP cache may serve the preload variant to a normal navigation.

All four methods reject with a DOMException or TypeError rather than throwing synchronously.

  • InvalidStateError: the registration has no active worker. Calling enable() in install (where the worker is still installing) rejects; activate is the earliest safe point, and navigator.serviceWorker.ready is the page-side equivalent.
  • TypeError from setHeaderValue(): value is not a valid HTTP header value (it must be a byte string without control characters or line breaks).
  • Not an exception: an unused preload response. If the handler returns a cached response without awaiting event.preloadResponse, the browser has already spent the request. Pass the promise to event.waitUntil() only when you need it consumed; otherwise accept the wasted request or disable preload for routes that are served from cache on every request.
  • Not an exception: event.preloadResponse resolving with undefined in a browser that supports the manager but has preload disabled for this registration. Code that assumes a Response throws TypeError: Cannot read properties of undefined on the first property access.

The three examples cover enabling, consuming, and detecting the feature; the second is the one every preload-enabled worker needs.

Enable once the worker is active, and keep the call inside waitUntil() so the activation does not finish before the setting is stored.

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

Because the setting persists on the registration, a later worker version that no longer calls enable() still has preload on; call disable() explicitly when removing it.

Using preloadResponse before falling back to fetch()

Section titled “Using preloadResponse before falling back to fetch()”

The handler serves a cache hit first, then the preloaded response, then a regular fetch. The preload promise is awaited on every path so the browser’s early request is consumed, not wasted.

self.addEventListener('fetch', (event) => {
if (event.request.mode !== 'navigate') return;
event.respondWith((async () => {
const cached = await caches.match(event.request);
if (cached) {
event.waitUntil(event.preloadResponse); // let the preload finish quietly
return cached;
}
const preloaded = await event.preloadResponse;
if (preloaded) return preloaded;
return fetch(event.request);
})());
});

Awaiting preloadResponse after a cache hit does not delay the response; it keeps the worker alive until the preload settles. A handler that ignores a pending preload makes Chrome log The service worker navigation preload request was cancelled before 'preloadResponse' settled. If you intend to use 'preloadResponse', use waitUntil() or respondWith() to wait for the promise to settle. in the page console.

Detecting the manager and keeping the fetch handler correct without it

Section titled “Detecting the manager and keeping the fetch handler correct without it”

Firefox before 99 and Safari before 15.4 have no navigationPreload on the registration, and event.preloadResponse is undefined (not a promise) in Chrome 58 and earlier. The pattern below works in all three states.

// activate: enable only when the manager exists
self.addEventListener('activate', (event) => {
event.waitUntil(
self.registration.navigationPreload
? self.registration.navigationPreload.enable()
: Promise.resolve() // no preload: navigations wait for the worker as before
);
});
// fetch: treat a missing preloadResponse as "no preload"
async function navigationResponse(event) {
const preloaded = event.preloadResponse ? await event.preloadResponse : undefined;
return preloaded ?? fetch(event.request);
}

The fallback costs the worker start-up time on every navigation, which is the problem preload solves; measure the worker’s boot time in the Performance panel before deciding whether a cache-first shell (which makes preload unnecessary) is the better fix.

Specifications

SpecificationStatus
None.