Back to blog

Your Analytics Script Is Probably Disabling the Back/Forward Cache

The back/forward cache makes back-button navigations near-instant, but one unload listener disables it for the whole page. Tracking scripts are the usual culprit — and CrUX now measures the damage.

The fastest navigation on the web is the one where nothing loads at all. The back/forward cache (bfcache) delivers exactly that — and a single line in your analytics vendor's script can switch it off for every page on your site.

bfcache is a browser optimization that keeps a full in-memory snapshot of a page — JavaScript heap included — when the user navigates away. Press the back button and the browser restores that snapshot and unpauses execution, producing a near-instant load with no network request. Back and forward navigations make up an estimated 10–20% of all navigations, so this is not an edge case.

One Listener Disqualifies the Whole Page

bfcache eligibility is fragile by design. The browser will not freeze a page that has registered an unload event listener, because unload implies the page expects to be torn down. On desktop, Chrome and Firefox make any page with an unload listener ineligible for bfcache — no exceptions, no partial credit.

The listener does not have to be yours. Third-party scripts registering unload from inside your page, or even inside a subframe, disqualify the top-level document. Lighthouse ships a dedicated no-unload-listeners audit precisely because the offending code is so often code the site author never wrote.

beforeunload is no longer disqualifying in modern browsers, but it is unreliable and still best avoided unless a user has unsaved changes.

Tracking Scripts Are the Usual Offender

The unload event is the classic place to fire a final beacon — flush a session, send a "page closed" event — so behavior-tracking and ad scripts reach for it constantly.

Facebook's fbevents.js registers an unload handler and appears on roughly 9% of all web pages according to HTTP Archive. PayPal's tag injects an iframe that adds an unload event, blocking bfcache on many checkout flows. Subframe scripts like hCaptcha have done the same. None of these require a change to your own code to start costing you — a vendor pushing an update is enough.

CrUX Now Shows You the Bill

This used to be invisible in field data. Since the March 2024 dataset, the Chrome User Experience Report (CrUX) reports a navigation_types breakdown — including the fraction of visits served from the back/forward cache — so you can see how many real users are missing the instant path.

The correlation is stark. CrUX analysis found a strong statistical relationship (ρ=0.87) between a high back_forward_cache fraction and instant_lcp_density — the share of loads with LCP under 200 ms. After Google's March 2026 update raised the ranking weight of LCP, INP, and CLS, a self-inflicted bfcache block is a measurable disadvantage at the 75th percentile, not a rounding error.

Detecting and Fixing It

Open DevTools, go to Application → Back/forward cache, and click Run Test. Chrome lists every blocking reason, including unload handlers added by third parties.

To detect bfcache restores in your own code, never use unload. Use pageshow and check persisted:

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    // restored from bfcache — no fresh page load fired
  }
})

To stop third parties from disqualifying you, set a response header that forbids unload listeners entirely:

Permissions-Policy: unload=()

This neutralizes unload handlers from any script — vendor tags, extensions, or your own legacy code — so the page stays bfcache-eligible regardless of what loads.

The Tracker That Never Touches It

The structural fix is to use instrumentation that has no reason to listen for unload in the first place. A privacy-first tracker records a pageview with a single keepalive request and lets the request outlive the page on its own — no teardown beacon, no unload handler, nothing for the browser to flag:

fetch('/collect', {
  method: 'POST',
  keepalive: true,
  body: JSON.stringify({ site_id, path: location.pathname })
})

When a tracker needs to react to a page being backgrounded, the correct signal is visibilitychange or pagehide, both of which fire without making the page bfcache-ineligible. Monoid's tracker is roughly 2 KB, sets no cookies, and hooks history.pushState for SPA route changes rather than the page lifecycle — so it has no unload listener to register and nothing to disable your back/forward cache.

Surveillance analytics taxes performance in ways that do not show up in a lab run. A bfcache block is one of the quietest: no error, no slow render, just a back button that reloads from the network when it should have restored from memory.

Sources

Comments

Loading comments…