# NavigationPreloadManager

> How registration.navigationPreload lets the browser fetch a navigation in parallel with service worker start-up, the enable(), disable(), setHeaderValue(), and getState() members, the InvalidStateError and TypeError conditions, and how FetchEvent.preloadResponse and the Service-Worker-Navigation-Preload header fit together.

`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

```js
// 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.

## 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

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.

:::observed
In Chrome (English UI), once `registration.navigationPreload.enable()` has resolved, reloading a controlled page shows the navigation twice in the Network panel: the preload request carries the request header `Service-Worker-Navigation-Preload: true` (visible under **Headers › Request Headers**), while the request issued by the worker's own `fetch()` does not; after `setHeaderValue('v2')` the header value changes to `v2` on the next navigation. `await registration.navigationPreload.getState()` in the worker console returns `{enabled: true, headerValue: 'v2'}`.
:::

## 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`

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

```js
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()`

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.

```js
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

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.

```js
// 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.

## See also

- [Service Workers specification: NavigationPreloadManager](https://w3c.github.io/ServiceWorker/#navigationpreloadmanager) (w3c.github.io)
- [Speed up service worker with navigation preloads](https://developer.chrome.com/blog/navigation-preload) (developer.chrome.com)
- [FetchEvent: preloadResponse property](https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/preloadResponse) (developer.mozilla.org)
- [FetchEvent and request routing](/reference/service-worker/fetch-event/)
- [Service worker lifecycle](/reference/service-worker/lifecycle/)
- [Navigation preload and start-up performance](/reference/performance/navigation-preload/)
- [Caching strategies for service workers](/reference/service-worker/caching-strategies/)