Back/Forward Cache (bfcache)

What the back/forward cache is — the browser feature that freezes a whole page in memory for instant Back/Forward navigation — how it differs from the HTTP cache, what blocks eligibility, how to test it, and its real (indirect) relationship to Core Web Vitals and SEO.

First published: Jul 3, 2026 · Last updated: Jul 17, 2026 · Advanced
demand #25 in Web Performance#231 in Technical SEO#311 on the site

The back/forward cache (bfcache) is a browser optimization that freezes a whole page — DOM, JavaScript heap, running state — in memory when you navigate away, so pressing Back or Forward can restore it instantly with no reload, no re-render, and no network requests, as long as the browser hasn't evicted that frozen snapshot first. It's a browser feature, not a search ranking factor: Google's own Core Web Vitals ranking docs never mention it. Its SEO relevance is indirect and scoped — a bfcache-restored navigation posts near-instant LCP and effectively zero CLS for the users who get one, which can improve your aggregate field Core Web Vitals on any site with meaningful Back/Forward traffic (1 in 10 desktop and 1 in 5 mobile navigations), but it doesn't guarantee your restore rate, your overall CWV rating, rankings, or conversion. The single biggest blocker is the unload event handler; the historically largest was Cache-Control: no-store, though Chrome now allows bfcache for many no-store pages conditionally as of its 2025 rollout. Test with Chrome DevTools for one-off lab checks, or the Chrome-only notRestoredReasons API for field data. Don't confuse bfcache with the HTTP cache, the browser's in-memory resource cache, a service worker's Cache Storage, or the old retired 'cached page' search feature.

TL;DR — Bfcache is a whole-page, in-memory snapshot (DOM + JS heap + running state), not a re-fetchable HTTP response, the browser’s in-memory resource cache, or a service worker’s Cache Storage — that’s the #1 conceptual distinction to nail. On navigating away the browser pauses JS and freezes the page; on Back/Forward, if that frozen snapshot is still available, it un-freezes and re-displays instantly with zero network requests — but eviction is always possible, so treat a restore as likely, not guaranteed. It is not a documented Google Search ranking factor (Search Central’s Core Web VitalsGoogle's three real-user UX metrics — LCP (loading), INP (responsiveness), and CLS (visual stability) — used by Google's ranking systems, with no official weight attached, measured on field data. docs never mention it); its relevance is indirect and scoped, through field CWV measurement (mainly LCPLargest Contentful Paint — render time of the largest visible image or text block, relative to when the page started loading. ≤2.5 s (at the 75th percentile) is good. and CLSCumulative Layout Shift — a unitless score for unexpected visual movement, taken from the largest burst (session window) of layout shifts, not the lifetime sum. ≤0.1 is good.) on the navigations that do get restored — it doesn’t guarantee your restore rate, aggregate CWV rating, rankings, or conversion. The biggest eligibility blocker is the unload handler (~18 percentage points of hit rate on Chrome); the historically largest was Cache-Control: no-store (blocking ~17% of mobile / ~7% of desktop history navigations), though Chrome now allows bfcache for many no-store pages conditionally (evicted on auth/cookie change, still blocked by the same open-connection APIs) after its 2025 rollout. Open connections, timers, and observers should be closed/paused at pagehide/freeze and re-established at pageshow/resume; window.opener, permissions policies, and frames can also block it — check the per-frame reason in DevTools or notRestoredReasons rather than assuming. Test one-off with Chrome DevTools; diagnose in the field with the Chrome-only notRestoredReasons API (a null result isn’t proof of a restore, and reason text isn’t stable). Each browser has its own eligibility rules, and SPAs’ soft navigations don’t get the same treatment.

Evidence for this claim The back-forward cache stores a complete page snapshot for instant history navigation. Scope: Current official or standards documentation. Confidence: high · Verified: web.dev: Back/forward cache Evidence for this claim Browser eligibility rules and APIs such as unload handlers can prevent bfcache use. Scope: Current official or standards documentation. Confidence: high · Verified: web.dev: bfcache eligibility

What bfcache actually is (the accuracy spine)

The single most important thing to get right: bfcache is a whole-page, in-memory snapshot, not a cached HTTP response. When you navigate away from a page, rather than tearing it down, the browser pauses JavaScript execution and freezes the entire page — the DOM, the JS heap, in-flight timers, the lot — and holds it in memory. If you press Back or Forward while that frozen snapshot is still available, the browser un-freezes it and re-displays the exact page you left, with zero network requests and zero re-render. That’s a possible restore, not a guarantee — the browser can evict the snapshot before you navigate back (memory pressure, a timeout, certain events), or a browser-specific rule can force a fresh load, in which case it’s just an ordinary history navigation instead. Google’s canonical framing is that bfcache is “a browser optimization that enables instant back and forward navigation.”

This is why conflating it with the HTTP/browser cache is the recurring competitor error. The HTTP cache stores responses to previous requests — files it can re-serve. Bfcache stores the live, running page. Chrome’s DevTools documentation draws the line explicitly: bfcache “differs from browser cache and HTTP cache.” You don’t “turn on” bfcache with cachingCaching stores a copy of a page or resource — in a browser, a CDN edge node, or a search crawler's own cache — so it can be served again without regenerating or re-downloading it. It isn't a direct ranking factor, but it feeds page speed and crawl efficiency. headers the way you configure HTTP caching — the only place headers enter the picture is that Cache-Control: no-store used to disqualify a page (more on that below).

The same distinction holds against two other caches people sometimes conflate with bfcache: the browser’s in-memory resource cache (compiled scripts and decoded images kept around for the current session) and a service worker’s Cache Storage (explicit request/response pairs a site manages itself via caches.open()). Both can be active on the same page at the same time as bfcache — neither one is bfcache, which is specifically the frozen page instance, not cached assets or intercepted responses.

One more disambiguation worth stating because it still causes confusion: bfcache has nothing to do with the old “cached page” search feature that Google (and Bing) once exposed in results. That was a stored snapshot of a page in the search indexStoring a crawled page in the search index so it can appear in results. Crawled is not the same as indexed — Google selects what to keep, and indexing isn't guaranteed., and it’s been retired. Bfcache is a client-side, renderingTurning HTML, CSS, and JavaScript into the final visual page and DOM.-engine feature.

How common are Back/Forward navigations, really?

This is not a niche edge case. Per web.dev, “1 in 10 navigations on desktop and 1 in 5 on mobile are either back or forward.” On any site with repeat/back-forward flows — e-commerce category-to-product-and-back, search results, paginated contentPagination splits a large set of content — product listings, blog archives, search results — across multiple sequentially numbered URLs. For SEO, each paginated page should be crawlable, indexable, and self-canonical; Google no longer uses rel=prev/next, but Bing still does., article-to-article reading — that’s a large slice of real navigations you can make near-instant.

Browser support

“All major browsers include a bfcache, including Chrome since version 96, Firefox and Safari.” Firefox and Safari have had their own, longer-running bfcache implementations; all Chromium-based browsers (Edge, Brave, Opera, Arc) inherit Chrome’s. Microsoft’s Edge policy documentation describes the same feature: when navigating away from a page, its current state (document tree, script, and so on) may be preserved in the back-forward cache, and if the browser navigates back the page may be restored from it and displayed in the state it was in before being cached. It’s enabled by default in Edge; the only off switch is an enterprise policy an IT admin controls, not a site owner.

The important caveat: each browser’s bfcache applies its own eligibility rules. A page that passes Chrome DevTools’ “Test back/forward cache” is not guaranteed to be eligible in Firefox or Safari. Treat a Chrome pass as necessary, not sufficient.

What blocks bfcache eligibility

The unload event — the single biggest blocker

If you take one thing from this article: stop using the unload event. web.dev puts it with rare emphasis for a Google doc: “Never use the unload event. Ever!” On Chrome, unload handlers cost roughly an 18-percentage-point reduction in bfcache hit rate — by far the largest self-inflicted disqualifier.

There are two reasons Chrome is actively deprecating it. First, it’s the biggest bfcache blocker. Second, unload is extremely unreliable to begin with: on mobile it frequently doesn’t fire at all, because tabs get backgrounded and killed and the browser prioritizes bfcache over firing unload. So the event you’re relying on for “cleanup” often never runs and it’s blocking a real performance win.

The fixes:

  • Replace unload with pagehide. The pagehide event fires in every case unload does, plus when a page enters the bfcache — so it’s a strict upgrade. Use visibilitychange for reliable “user is leaving” cleanup.
  • Detect a bfcache restore with pageshow. Listen for pageshow and check event.persisted — if it’s true, the page was restored from bfcache, which is your cue to refresh stale data or re-count a pageview.
  • Proactively block unload listeners with the Permissions-Policy: unload=() response header, which prevents any unload handlers from being registered at all. Chrome is gradually migrating the default policy toward deny (a Permissions-Policy for unload shipped from Chrome 115).

Cache-Control: no-store — historically the largest, now nuanced

This is the freshness point most competitor content gets wrong. Historically, Cache-Control: no-store was the largest single reason pages were excluded from bfcache — Chrome’s own figures put it at roughly 17% of history navigations on mobile and 7% on desktop. Many sites set no-store defensively to avoid serving a stale page, but Google’s argument is that this rationale weakens under bfcache: a bfcache restore isn’t loading a stale cached response, it’s re-displaying the exact live page almost as if the tab had been left open.

So Chrome changed the behavior — but conditionally, not universally. Experiments began in Chrome 116, with the final rollout to 100% of users over March and April 2025: Chrome now allows bfcache for many no-store pages, subject to specific safety conditions rather than a blanket exception. Per Chrome’s own docs, this comes with real constraints — the page is evicted from bfcache if authentication state or cookies change while it’s frozen (so a signed-out or cookie-cleared visitor won’t see a stale signed-in snapshot), and a fixed list of APIs — the same open-connection APIs covered below (IndexedDB, WebSocket, WebRTC, and the rest) — still exclude a no-store page from bfcache the same way they would any other page. This is Chrome-specific behavior on a specific version range, not a rule you can assume other browsers, or older Chrome versions, follow — pull the current DevTools/notRestoredReasons report for the browser and version you’re actually testing rather than trusting a fixed rule of thumb. The practical takeaway: any guide (including older versions of this one) that lists no-store as an unconditional, permanent bfcache blocker is stale — and so is treating it as fully solved. If freshness genuinely matters for a page, Chrome’s docs suggest no-cache or a short max-age (e.g. max-age=60) rather than no-store.

Open connections, observers, and other blockers

At the moment of navigation, certain open resources can still block eligibility — and exactly which ones, and whether they block outright or just get closed-and-reconnectable, is browser- and version-specific. Treat the list below as examples of a pattern, not a fixed, permanent blocker list:

  • In-progress fetch() / XMLHttpRequest requests.
  • Open IndexedDB transactions.
  • Open WebSocket / WebRTC connections, timers, and observers (MutationObserver, IntersectionObserver, and similar). This is an actively improving area — recent Microsoft Edge release notes show an open WebSocket now being closed when a page enters bfcache (rather than blocking caching outright), with reconnection recommended via the pageshow event’s event.persisted check. That mirrors Chrome’s broader trend of reducing blockers rather than just excluding pages.

The general pattern to build to, rather than memorizing a fixed list: close or pause open connections, timers, and observers in your pagehide/freeze handling, and re-establish them in your pageshow/resume handling when event.persisted is true. That pattern survives a browser changing exactly which APIs block eligibility outright versus which ones it now just suspends and lets you reconnect.

window.opener, permissions policies, and frames. A window.opener reference, certain permissions policies, and embedded (same- or cross-origin) iframesHTML element that displays one webpage inside another — how embeds work. can also affect eligibility — this is on the checklist I distilled in Ahrefs’ CLS guide and in Chrome’s docs. But don’t assume which one is the actual cause from a generic checklist: Chrome’s DevTools panel and the notRestoredReasons API both report blocking reasons per frame — the top frame and each iframe separately — because the frame actually responsible for a block isn’t always the top-level page. Pull the real reason from that frame-level report for the browser you’re testing rather than guessing from a general list.

Getting the lifecycle event/state sequence right

Mixing up what each lifecycle event actually proves versus merely suggests is the second most common correctness bug here, after the eligibility mistakes above:

Event / stateSignalWhat it actually meansWhat to do
pagehide (event.persisted === true)Intent to cacheThe browser is attempting to freeze the page for bfcache — not a confirmed cache entryClose/pause connections, timers, and observers here; don’t assume the page will actually be restored
freezePausedJS execution is paused; the page can still be evicted before any restoreNo action beyond what you already did at pagehide
(no event) possible evictionThe browser can drop a frozen page from memory at any point — memory pressure, a timeout, a browser rule — and there’s no event that fires for thisDon’t rely on cleanup code running later; do it unconditionally at pagehide/freeze
pageshow (event.persisted === true)Confirmed restoreThe only reliable signal that a bfcache restore actually happenedRefresh time-sensitive/sensitive state, reconnect closed connections, count exactly one analytics view
resumeResumedJS execution is unpaused after a confirmed restoreReconnect anything paused at freeze

The practical rule: treat pagehide.persisted as intent, not proof — the page might still get evicted before you’d ever see a restore. Only pageshow.persisted === true is evidence a restore happened. Do your cleanup unconditionally at pagehide/freeze (it’s cheap and safe even on an ordinary navigation), and do your restore-specific work only at pageshow/resume, gated on event.persisted, so you don’t refresh data or double-count an analytics view on a plain fresh load.

How to test and diagnose bfcache

Lab / one-off: Chrome DevTools

Open DevTools → Application → Background services → Back/forward cache, then click “Test back/forward cache.” Chrome auto-navigates to chrome://terms/ and back, then reports either success or a specific list of blocking reasons. Good for checking one URL at a time.

Field / production: the notRestoredReasons API

Previously the only way to check eligibility was that manual, one-URL-at-a-time DevTools test — there was no way to see why real users’ navigations were blocked. The notRestoredReasons property on PerformanceNavigationTiming (shipped in Chrome 123+) closes that gap: it reports the specific blocking reason(s) for the top frame and same-origin iframes in real field dataPerformance metrics captured from real users, not lab tests..

const nav = performance.getEntriesByType('navigation')[0];
console.log(nav.notRestoredReasons);

A few things to get right when you use it, straight from Chrome’s own API guidance:

  • It’s Chrome-only (123+). Firefox and Safari don’t expose an equivalent field API, so you still need manual spot-checks in those browsers to know your restore rate there.
  • A null result is ambiguous, not a green light. It can mean the page was restored, or it can mean the browser simply didn’t collect a reason — Chrome’s own docs say not to treat null as proof of a successful restore.
  • Reason text isn’t a stable contract. Don’t hard-code string matches against it; group and trend by reason instead, since the exact wording can change across Chrome versions.

Practically: pull notRestoredReasons alongside pageshow.persisted restore rates before and after a fix ships, compare the trend rather than a single snapshot, and pair it with a manual DevTools/lab pass in Firefox and Safari for the browsers the API can’t reach. This is the tool to reach for when diagnosing bfcache at scale in RUM/production, rather than checking URLs one by one — just don’t treat it as the whole picture.

Bfcache and Core Web Vitals — the precise relationship

Here’s the nuance most competitor content blurs, and the angle worth owning.

How a bfcache restore is measured. Browsers (and therefore CrUXChrome User Experience Report — Google's public dataset of real-world (field) performance data from eligible Chrome users. It's the official field-data source behind the Core Web Vitals program. field data) count a bfcache-restored navigation as an extremely fast “page load” — near-instant LCPLargest Contentful Paint — render time of the largest visible image or text block, relative to when the page started loading. ≤2.5 s (at the 75th percentile) is good. and, when the page is implemented correctly (nothing has to re-lay-out), effectively zero additional CLSCumulative Layout Shift — a unitless score for unexpected visual movement, taken from the largest burst (session window) of layout shifts, not the lifetime sum. ≤0.1 is good., because there’s no re-render. In DebugBear’s real-world comparison, a bfcache-restored page hit LCPLargest Contentful Paint — render time of the largest visible image or text block, relative to when the page started loading. ≤2.5 s (at the 75th percentile) is good. around 100ms versus ~427ms for an uncached load. This is exactly why bfcache shows up as a lever on CLSCumulative Layout Shift — a unitless score for unexpected visual movement, taken from the largest burst (session window) of layout shifts, not the lifetime sum. ≤0.1 is good. and LCP in my own Ahrefs CLS checklist, where I put it simply: “Make sure your pages are eligible for bfcache. The back/forward cache keeps pages in the browser cache. It allows for instant loading of a page that was already loaded, meaning no layout shifts will happen.”

Two scope caveats worth being precise about, since this is where competitor content tends to overclaim. First, this only affects navigations CrUXChrome User Experience Report — Google's public dataset of real-world (field) performance data from eligible Chrome users. It's the official field-data source behind the Core Web Vitals program. classifies as back/forward (its navigation-type dimension) — it says nothing about your first-visit or reload navigations, which are the majority of traffic on most sites. Second, a restore improving the measured experience for the users who get one is not the same as a guarantee: a user who’s evicted from bfcache (see the lifecycle table above) still gets a normal, unimproved load, so bfcache eligibility work moves your restore rate among Back/Forward navigations, not a fixed share of your total traffic — and it doesn’t guarantee your aggregate field Core Web VitalsGoogle's three real-user UX metrics — LCP (loading), INP (responsiveness), and CLS (visual stability) — used by Google's ranking systems, with no official weight attached, measured on field data. rating, your rankings, or your conversion rate. It’s a real, measurable lever with a specific, bounded scope, not a general performance or SEO fix.

Is bfcache a ranking factor? No. This is the defensible, differentiated claim. Google’s own Core Web Vitals ranking documentation does not mention bfcache at all. The honest chain of influence is: bfcache eligibility → better field CWV numbers (mainly LCP/CLS) on back/forward navigations → Core Web VitalsWeb Vitals is Google's initiative (launched May 2020) for unified page-experience quality signals. Core Web Vitals — LCP, INP, and CLS — are the subset used in ranking; the rest (TTFB, FCP, TBT, Speed Index) are diagnostic, not ranking factors. is one input among many “page experience” signals Google says align with what its ranking systems already reward. That’s a materially weaker, more precise claim than “bfcache boosts rankings” — and it’s the claim competitor content should be making but usually isn’t careful about. Bfcache is also, correctly, a renderingTurning HTML, CSS, and JavaScript into the final visual page and DOM.-engine feature, not a crawlerA crawler — also called a spider or bot — is an automated program that fetches web pages, extracts their links, and queues new URLs to visit. Search engines use crawlers to discover and download content for their index. feature — it has nothing to do with how GooglebotGooglebot is Google's web crawler — the software that fetches pages so Google can index and rank them. It comes in two variants, Googlebot Smartphone (primary, under mobile-first indexing) and Googlebot Desktop, and runs an evergreen Chromium renderer. or BingbotBingbot is Microsoft Bing's primary web crawler — the bot that discovers, fetches, and renders pages to build the Bing index. That index also powers Yahoo, DuckDuckGo, Ecosia, and Microsoft Copilot, so Bingbot's reach is far wider than Bing's own search-market share. crawl your pages, which is why there’s no “Bing’s take on bfcache for SEO” the way there is for robots.txtA plain-text file at the root of a host that tells crawlers which URLs they may and may not request. It controls crawling, not indexing — a blocked URL can still be indexed if it's linked from elsewhere. or sitemapsA sitemap is a file that lists the pages, images, videos, and other files on your site so search engines can discover them. It helps discovery, but submitting a sitemap doesn't guarantee crawling or indexing..

SPAs and soft navigations. Bfcache operates on real browser navigation and history events. A single-page app’s client-side “soft” route change (a JS-driven view swap that doesn’t trigger an actual browser navigation) is not a bfcache event and doesn’t get the same treatment. Some RUM tools’ attempts to attribute Core Web Vitals to soft navigations can create CrUX-vs-RUM measurement mismatches — worth a callout if you’re auditing a JS-framework site.

How common are bfcache blockers in the wild?

The HTTP Archive Web Almanac tracks this, and it’s a live, moving area — not settled old news. Back in the 2022 edition, at least ~22% of mobile pages were ineligible for bfcache on the unload and no-store criteria alone. Since then, unload handler usage has been declining across site tiers and devices — but Cache-Control: no-store usage has risen (the 2025 chapter puts it around 23% of sites, up from ~21% in 2024, attributed partly to more authenticated/personalized experiences and stricter compliance requirements).

The counter-intuitive finding worth citing: larger, higher-traffic sites are disproportionately more likely to block their own bfcache. On the top 1,000 sites, around 28% of desktop pages and 20% of mobile pages still use unload handlers, versus only ~11% desktop / ~10% mobile across all sites — often because bigger sites carry more legacy analytics and unload-dependent code. The sites with the most back/forward traffic to lose are frequently the ones still getting in their own way.

Where this sits

Bfcache is one performance lever among several in this cluster. Its payoff shows up in Core Web VitalsGoogle's three real-user UX metrics — LCP (loading), INP (responsiveness), and CLS (visual stability) — used by Google's ranking systems, with no official weight attached, measured on field data. field data — specifically in Cumulative Layout ShiftCumulative Layout Shift — a unitless score for unexpected visual movement, taken from the largest burst (session window) of layout shifts, not the lifetime sum. ≤0.1 is good. and Largest Contentful PaintLargest Contentful Paint — render time of the largest visible image or text block, relative to when the page started loading. ≤2.5 s (at the 75th percentile) is good., since a restored page re-displays instantly with no re-layout. It’s distinct from cachingCaching stores a copy of a page or resource — in a browser, a CDN edge node, or a search crawler's own cache — so it can be served again without regenerating or re-downloading it. It isn't a direct ranking factor, but it feeds page speed and crawl efficiency., which stores files rather than a live page snapshot, even though the two share the Cache-Control header as a touchpoint. There’s no direct tie to Interaction to Next PaintInteraction to Next Paint — the input-to-paint latency at the 75th percentile of a page's interactions. ≤200 ms is good., so I won’t force one.

Add an expert note

Pin an expert quote

New person? Create their unclaimed profile at /admin/experts/ → Pin a quote first.