# Back/forward cache (bfcache)

> How the back/forward cache restores a whole page from memory, which listeners and headers make a page ineligible, and how notRestoredReasons reports why.

The back/forward cache (bfcache) stores a complete snapshot of a page, including its
JavaScript heap and in-flight state, when the user navigates away, and restores it instantly on
a later back or forward navigation instead of reloading. A restore skips the network and script
execution entirely, which is faster than any service worker or HTTP cache hit, but only pages
that avoid a short list of blockers are eligible.

## How it works

On navigation away, the browser freezes the page: timers and pending promises are paused, not
cancelled, and the document stays in memory for a bounded time. On a back or forward
navigation to that entry the page is thawed in place; `pageshow` fires with `persisted === true`
and no `load` event. If the page was evicted (memory pressure, a time limit, or a blocker), the
navigation is an ordinary load ([Back/forward cache](https://web.dev/articles/bfcache), web.dev).
Every engine ships a bfcache: Chrome since version 96 on desktop and Android, Firefox and Safari
for far longer.

### What makes a page ineligible

- **An `unload` listener.** Desktop Chrome and Firefox refuse to cache a page that registers
  one; Safari and mobile Chrome may cache it and then skip firing the event, which makes the
  handler unreliable in every engine. `pagehide` fires in every case `unload` would and also on
  entry to the cache, so it is the replacement.
- **`Cache-Control: no-store` on the main document.** Browsers have historically refused to
  cache such pages even though the bfcache is not an HTTP cache, so the header belongs only on
  pages whose content must not be stored in any form.
- **Open connections.** An open `WebSocket`, `WebRTC` connection, or IndexedDB transaction at
  the moment of navigation blocks caching in Chrome; close them in `pagehide`.
- **Other blockers.** `window.opener` references kept alive, a pending `beforeunload` prompt, and
  in-progress `fetch()` requests with a body in some versions. Chrome's list is long and
  reported per navigation by the API below.

### Reading why a page was not restored

`performance.getEntriesByType('navigation')[0].notRestoredReasons` (Chrome 125; BCD
`api.PerformanceNavigationTiming.notRestoredReasons`) is `null` after a restore and otherwise an
object whose `reasons` array names the blockers, such as `{ reason: "unload-listener" }`, with
the same tree for cross-origin iframes redacted to `null`. Firefox and Safari do not expose it;
the DevTools tester below covers Chrome during development, and the field API covers production.

### Support position

Every engine ships a bfcache: Safari and Firefox have had one for over a decade, and Chrome
has had it on every platform since version 96 ([Back/forward cache](https://web.dev/articles/bfcache),
web.dev). The observable differences are in the
eligibility rules above and in `notRestoredReasons`, which only Chromium exposes.

### Interaction with service workers and Core Web Vitals

A bfcache restore does not consult the service worker or the HTTP cache at all, and a restore is
counted as a separate page view in Core Web Vitals tooling with a near-zero LCP, which is why
bfcache eligibility improves field metrics directly. Sensitive state (a signed-out session, a
stale cart total) must be refreshed in a `pageshow` handler that checks `event.persisted`,
because `load` will not fire.

## Examples

Both examples live in the page, not the service worker, because the bfcache is a document-level feature.

### Refreshing stale state on restore

`pageshow` runs on initial load and on restore; `persisted` tells them apart. The handler
re-checks the session and re-fetches data whose freshness matters without reloading the page.

```js
window.addEventListener('pageshow', (event) => {
  if (!event.persisted) return; // ordinary load: the load handler already ran
  refreshSession();
  refreshCartTotal();
});

window.addEventListener('pagehide', (event) => {
  if (event.persisted) {
    socket?.close(); // the page is entering the bfcache: release the connection
  }
});
```

Closing the WebSocket in `pagehide` is what makes the page eligible in Chrome; reopening it in
`pageshow` restores the live state the user expects.

### Reporting blockers from the field

Send `notRestoredReasons` to analytics on pages that were not restored, so the blockers that
matter in production (third-party scripts registering `unload`, for example) are visible.

```js
function reportBfcacheBlockers() {
  const [nav] = performance.getEntriesByType('navigation');
  if (!nav || !('notRestoredReasons' in nav)) {
    return; // not supported (Firefox, Safari): rely on the developer tools test instead
  }
  if (nav.type === 'back_forward' && nav.notRestoredReasons) {
    navigator.sendBeacon('/analytics/bfcache', JSON.stringify(nav.notRestoredReasons));
  }
}
```

Filter on `nav.type === 'back_forward'`: on other navigation types the property is present but
describes nothing the page could have changed.

:::observed
Chrome DevTools, Application > Background services > Back/forward cache, has a **Test
back/forward cache** button that navigates away and back and then reports either that the page
was restored or a list of blockers grouped as **Actionable**, **Pending Support**, and **Not
Actionable** ([Test back/forward cache](https://developer.chrome.com/docs/devtools/application/back-forward-cache),
developer.chrome.com). A page with `window.addEventListener('unload', () => {})` lists the
unload listener under **Actionable**; removing the listener and re-running the test flips the
result to restored on the same build.
:::

## See also

- [Back/forward cache](https://web.dev/articles/bfcache) (web.dev)
- [Test back/forward cache](https://developer.chrome.com/docs/devtools/application/back-forward-cache) (developer.chrome.com)
- [PerformanceNavigationTiming: notRestoredReasons property](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming/notRestoredReasons) (developer.mozilla.org)
- [Core Web Vitals (LCP, INP, and CLS)](/reference/performance/core-web-vitals/)
- [Speculation Rules API](/reference/performance/speculation-rules/)
- [PerformanceObserver and paint timing](/reference/performance/startup-performance/)