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.
Syntax
Section titled “Syntax”// 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 handlerconst preloaded = await event.preloadResponse; // Response or undefinedPreload 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.
Members
Section titled “Members”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.
Exceptions
Section titled “Exceptions”All four methods reject with a DOMException or TypeError rather than throwing synchronously.
InvalidStateError: the registration has no active worker. Callingenable()ininstall(where the worker is still installing) rejects;activateis the earliest safe point, andnavigator.serviceWorker.readyis the page-side equivalent.TypeErrorfromsetHeaderValue():valueis 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 toevent.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.preloadResponseresolving withundefinedin a browser that supports the manager but has preload disabled for this registration. Code that assumes aResponsethrowsTypeError: Cannot read properties of undefinedon the first property access.
Examples
Section titled “Examples”The three examples cover enabling, consuming, and detecting the feature; the second is the one every preload-enabled worker needs.
Enabling preload in activate
Section titled “Enabling preload in activate”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 existsself.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.
See also
Section titled “See also”- Service Workers specification: NavigationPreloadManager (w3c.github.io)
- Speed up service worker with navigation preloads (developer.chrome.com)
- FetchEvent: preloadResponse property (developer.mozilla.org)
- FetchEvent and request routing
- Service worker lifecycle
- Navigation preload and start-up performance
- Caching strategies for service workers
Specifications
| Specification | Status |
|---|---|
| None. | |