Skip to content

Performance · Concept

Back/forward cache (bfcache)

Published

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.

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, web.dev). Every engine ships a bfcache: Chrome since version 96 on desktop and Android, Firefox and Safari for far longer.

  • 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.

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.

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

Section titled “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.

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

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.

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.

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.

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.

Specifications

SpecificationStatus
None.