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.
How it works
Section titled “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, 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
Section titled “What makes a page ineligible”- An
unloadlistener. 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.pagehidefires in every caseunloadwould and also on entry to the cache, so it is the replacement. Cache-Control: no-storeon 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,WebRTCconnection, or IndexedDB transaction at the moment of navigation blocks caching in Chrome; close them inpagehide. - Other blockers.
window.openerreferences kept alive, a pendingbeforeunloadprompt, and in-progressfetch()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
Section titled “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
Section titled “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,
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.
Examples
Section titled “Examples”Both examples live in the page, not the service worker, because the bfcache is a document-level feature.
Refreshing stale state on restore
Section titled “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.
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
Section titled “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.
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.
See also
Section titled “See also”- Back/forward cache (web.dev)
- Test back/forward cache (developer.chrome.com)
- PerformanceNavigationTiming: notRestoredReasons property (developer.mozilla.org)
- Core Web Vitals (LCP, INP, and CLS)
- Speculation Rules API
- PerformanceObserver and paint timing
Specifications
| Specification | Status |
|---|---|
| None. | |