sfw/fix
bfcache restore failed medium

Page prevented back/forward cache restoration

The page can't be stored in the back/forward cache, so hitting Back reloads it from the network instead of restoring instantly.

What you see

Page prevented back/forward cache restoration
Failure reason: pages with an unload handler are not currently eligible for back/forward cache.

What’s actually happening

You click Back and instead of the previous page snapping into view in a few milliseconds, it spins through a full reload: blank screen, scripts re-run, scroll position lost. Lighthouse flags it under the bfcache audit and Chrome DevTools shows it in the Application > Back/forward cache panel. The hit affects perceived speed on every back/forward navigation, not first load.

Common causes

  • An `unload` event listener anywhere on the page (or in a third-party script/iframe) — the single most common blocker.
  • `Cache-Control: no-store` on the main document combined with a state change Chrome tracks (a cookie write or HTTPS-only cache entry).
  • A still-open connection at navigation time: an in-flight `fetch`, an open WebSocket, WebRTC, or a pending IndexedDB transaction.
  • `window.opener` being set (page opened via `target="_blank"` without `rel="noopener"`), or use of the legacy `Cache-Control: no-cache` patterns that keep the page alive.
  • An extension or embedded widget injecting `beforeunload`/`unload` handlers you don't control.

How to fix it

  1. Find the real reason in DevTools, don't guessOpen DevTools > Application > Back/forward cache, click Test back/forward cache (or just navigate away and back). Chrome lists the exact blocking reasons with the offending frame. In production, read `performance.getEntriesByType('navigation')[0].notRestoredReasons` (Chrome 123+) to collect the same data via RUM.
  2. Replace unload with pagehideSearch the codebase for `addEventListener('unload'` and `onunload`. Swap each for `pagehide`, and move teardown/analytics-beacon logic into `pagehide` or `visibilitychange` with `document.visibilityState === 'hidden'`. `pagehide` fires reliably and does not disqualify bfcache.
  3. Close open connections on pagehideIn a `pagehide` handler, call `socket.close()` on WebSockets, abort in-flight fetches with an `AbortController`, and let IndexedDB transactions finish. Reopen them in `pageshow` when `event.persisted` is true so a restored page reconnects.
  4. Stop sending no-store on cacheable HTMLIf the document carries `Cache-Control: no-store` only out of habit, switch to `no-cache` or a short `max-age` with `private`. Reserve `no-store` for genuinely sensitive pages (banking, account settings) where bfcache is undesirable anyway.
  5. Neutralize third-party blockersIf an analytics or chat widget registers `unload`, update it to a version that uses `pagehide`, or load it in a way that doesn't taint the top frame. Confirm the fix by re-running the DevTools test.

Stop it recurring

Lint for `unload`/`beforeunload` in CI and add a Lighthouse bfcache assertion so a regression fails the build instead of shipping silently.

Related errors