ESC

Type to search the knowledge base.

BFCache Back Forward Cache

How the back/forward cache freezes pages for instant history nav, what blocks it, and how to restore state safely.

advanced3 min read
  • 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:

  1. Freeze the outgoing page (pause timers, media, connections as defined by the browser).
  2. Keep it off-screen in bfcache when eligible.
  3. On return, unfreeze and fire pageshow with event.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

  1. Search the codebase for unload and beforeunload — remove unless legally required (and accept bfcache loss).
  2. On pagehide with persisted, avoid tearing down storage that the frozen page still needs.
  3. Analytics: dual-path visibilitychange + pagehide so mobile backgrounding still flushes.
  4. After restore, invalidate time-sensitive UI (OTP countdown clocks, WebSocket session ids, “last synced” labels).
  5. 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).

Further reading

Related guides