BFCache Back Forward Cache
How the back/forward cache freezes pages for instant history nav, what blocks it, and how to restore state safely.
- browser
- bfcache
- performance
- page-lifecycle
Hit Back on a well-built site and the previous page often reappears instantly — not because the server was fast, but because the browser restored a frozen snapshot of the document from the back/forward cache (bfcache). That is a full in-memory page (DOM, JS heap, scroll) parked while the user is elsewhere, then resumed on history navigation.
Docs: web.dev bfcache, MDN pageshow, Page Lifecycle.
Mental model
Normal navigation: tear down document → fetch new HTML → rebuild. History traversal (back/forward) can instead:
- Freeze the outgoing page (pause timers, media, connections as defined by the browser).
- Keep it off-screen in bfcache when eligible.
- On return, unfreeze and fire
pageshowwithevent.persisted === true.
Users feel “instant.” Analytics and SPA state often do not notice unless you listen.
What blocks bfcache
Eligibility is browser-specific, but common disqualifiers:
| Blocker | Why |
|---|---|
unload handlers |
Legacy; browsers treat them as “page might care about teardown” |
Open WebSocket / some active connections |
Hard to freeze cleanly |
Cache-Control: no-store on the main document (varies) |
Privacy / freshness policy |
Heavy beforeunload usage |
Similar friction to unload |
| Some iframe / opener combinations | Cross-document complexity |
// Anti-pattern for bfcache-friendly apps
window.addEventListener('unload', () => {
navigator.sendBeacon('/analytics', payload);
});
Prefer pagehide / visibilitychange + sendBeacon without unload:
window.addEventListener('pagehide', (e) => {
// e.persisted === true means page may enter bfcache
navigator.sendBeacon(
'/analytics',
JSON.stringify({ type: 'leave', persisted: e.persisted }),
);
});
Restoring UI correctly
When the page comes back from bfcache, old JS state is still there. That is usually good (form fields intact) and sometimes bad (stale “online” flags, expired tokens assumed valid, frozen carts).
window.addEventListener('pageshow', (event) => {
if (!event.persisted) return; // normal load, not bfcache restore
void refreshSession();
reconnectRealtime();
});
Re-check feature flags and A/B assignments if they can change while the snapshot sits frozen.
Measuring eligibility
Chrome DevTools → Application → Back/forward cache: navigate away and back, see reasons for not restorable. In the field, watch pageshow with persisted, and notice full reloads on Back that should have been restores.
SPA notes
Client-side routers still live inside one document. bfcache applies to document-level history entries. Cross-document navigations (MPA, hard links) are where bfcache shines. Hybrid apps: don’t register unload “just in case,” and avoid leaving long-lived connections open if restore matters.
Interview out-loud
“bfcache freezes a full page for back/forward so restore is near-instant. unload and some open connections block it. On restore, pageshow fires with persisted true — revalidate session and reconnect, don’t assume a cold start.”
Production checklist
- Search the codebase for
unloadandbeforeunload— remove unless legally required (and accept bfcache loss). - On
pagehidewithpersisted, avoid tearing down storage that the frozen page still needs. - Analytics: dual-path
visibilitychange+pagehideso mobile backgrounding still flushes. - After restore, invalidate time-sensitive UI (OTP countdown clocks, WebSocket session ids, “last synced” labels).
- Document for your team: “Back is not always a cold load.”
Debug transcript (what you’ll say on a call)
Open Application → Back/forward cache, navigate to another origin on the same tab, hit Back. If the panel lists “unload handler,” delete it and retest. Confirm pageshow logs persisted: true. If your SPA still does a full network reload on Back, you may be forcing a navigation type that never enters bfcache (hard redirect, location.replace chains, or Cache-Control: no-store on HTML).
Related
Further reading
Related guides
- Page Lifecycle Eventsactive, passive, hidden, frozen, discarded — Page Lifecycle states and the events you should actually use.
- Browser DevTools Network PanelRead waterfalls, timing phases, headers, throttling, and initiator chains in the Network panel like a production debugger.
- Browser DevTools Performance PanelRecord main-thread timelines: long tasks, style/layout/paint, frames, and how to turn flame charts into INP fixes.
- Browser Networking 101DNS, TCP/TLS, HTTP/1.1 vs H2/H3, connection reuse, and what frontend code can actually influence.
- Compositor LayersWhen browsers promote layers, why transform/opacity animate cheaply, and layer explosion costs.