DEV Community

Cover image for Back/Forward Cache (BFCache): Why Back Navigation Breaks Your Performance Story
Apogee Watcher
Apogee Watcher

Posted on Originally published at apogeewatcher.com

Back/Forward Cache (BFCache): Why Back Navigation Breaks Your Performance Story

Open the same URL in two different ways and the numbers disagree. A cold PageSpeed Insights or Watcher lab run downloads HTML, CSS, and JavaScript under throttling and reports Largest Contentful Paint, Interaction to Next Paint, and Cumulative Layout Shift for that load. A shopper who taps Back after a product click may never run that load again: Chrome, Firefox, and Safari can restore a frozen snapshot of the previous page from the back/forward cache (bfcache) in roughly the time it takes to paint what was already in memory.

When that restore succeeds, the visit feels instant. When something on the page blocked bfcache, the browser starts a normal navigation instead, and the client sees a second full load that never appeared in the cold-load PDF. Agencies that only report lab medians on first navigations therefore miss a large slice of real browsing. Chrome’s own usage notes put back or forward navigations at about one in ten desktop visits and one in five on mobile. Metric definitions for LCP, INP, and CLS still sit in LCP, INP, CLS: What Each Core Web Vital Means and How to Fix It; bfcache decides whether many of those return trips even run a full paint path.

What the back/forward cache is (and is not)

BFCache is a browser optimisation for history navigations. When the user leaves a page, the browser can keep the document, the JavaScript heap, and scroll position in memory, pause timers and most pending work, and restore that snapshot if the user comes back soon. That is different from the HTTP cache. The HTTP cache stores responses so a repeat network load can skip some downloads. BFCache stores a live page so a history navigation can skip the network path entirely when the restore succeeds.

web.dev is explicit that even a well-warmed HTTP cache is slower than a bfcache restore, because a normal reload still has to rebuild the page. Soft navigations inside a single-page app do not use bfcache the same way: history entries that stay inside client routing are not full document navigations. Full document exits from an SPA can still enter bfcache and speed up the return to that shell.

For retainer language, bfcache sits as a third clock beside lab Lighthouse and field CrUX. Lab schedules answer what a cold mobile load looks like under throttling. Field Core Web Vitals answer what eligible Chrome users experience at p75. BFCache answers whether Back or Forward gets a memory restore or another full load. Those answers belong on separate report lines so a client cannot collapse them into one green screenshot. Mix the clocks and a healthy homepage Lighthouse run can sit next to “the site feels slow when I go back.”

Why back navigation breaks the performance story

Cold synthetic tests are useful, but they are incomplete. Scheduled PageSpeed Insights runs, including Watcher’s multi-page domain reports, measure fresh navigations. They do not simulate a user browsing a category page, opening a product, then returning. That history path is exactly where bfcache either saves the session or forces a second expensive load.

There is a second reporting trap. When bfcache is working, many fast history restores never become “page loads” in analytics and RUM the way a cold navigation does. web.dev notes that enabling bfcache can remove some of the fastest repeat visits from load-time datasets, which can make aggregate load metrics look worse even while users feel better. Teams that only watch LCP distributions without tagging navigation type can chase cold-load LCP while history restores stay blocked, or they can panic about a slower average after bfcache starts working. Neither reaction matches what the user felt on Back.

An honest client pack usually separates three lines:

  1. Cold lab Lighthouse on homepage, pricing, and checkout (or the lead templates the retainer covers) for regression and budget work.
  2. Field CrUX where Google returns it (Search experience).
  3. BFCache eligibility on the same URLs (history UX), verified in DevTools or Lighthouse’s bf-cache audit.

PageSpeed Insights vs automated monitoring covers why one-off PSI pastes fail as a portfolio system of record. When to use synthetic vs real user monitoring covers lab versus RUM. BFCache sits beside both: synthetic does not replace a history-navigation check, and RUM only helps when restores and blockers are annotated in the data.

How to detect a bfcache restore with pageshow persisted

The browser fires pageshow after a normal load and after a bfcache restore. The event’s persisted flag is true when the page came back from bfcache and false on a normal load. That single boolean is the usual hook for analytics, cleanup, and reconnecting live features.

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    // Restored from bfcache: refresh auth UI, reopen sockets, re-send page_view if needed.
  }
});

Enter fullscreen mode Exit fullscreen mode

pagehide (not unload) is the event that fits when state needs flushing as the user leaves. On pagehide, persisted being true means the browser intends to keep the page for bfcache; it is not a guarantee that the snapshot will stay. Teams that still need a beacon usually pair pagehide with visibilitychange when document.visibilityState === 'hidden'. unload stays off that path because it is unreliable on mobile and is a common bfcache blocker.

WebSockets, EventSource, and similar live channels usually reopen inside the pageshow branch where event.persisted is true. Closing those connections on pagehide and reopening on restore is the pattern that lets the page enter bfcache without leaving a live socket open. Skipping that pair is a common reason chat widgets and live pricing feeds look fine on first paint and then silently block history restores.

What blocks bfcache: unload, no-store, and open connections

Not every page is eligible. Chrome’s DevTools and Lighthouse group failure reasons into actionable items you can fix, pending browser support, and non-actionable internal cases. Agency work usually starts with the actionable list.

Unload listeners. An unload handler is a classic blocker. Themes, older analytics snippets, and “exit intent” scripts still register it. pagehide and visibilitychange cover the same flush needs without that cost. Chrome also documents a Permissions-Policy approach (unload=()) that stops extensions or third parties from re-adding unload handlers on pages you control.

Cache-Control: no-store on the main document. Chrome will not put a document with no-store into bfcache. Teams often apply no-store broadly on HTML to keep authenticated pages “fresh.” For pages that should remain bfcache-eligible, no-cache or a short max-age still lets the browser revalidate on normal navigations without forbidding the memory snapshot. no-store fits responses that must never be stored. Document-level caching guidance pairs with Cache-Control headers for web performance; bfcache adds the constraint that no-store on HTML is not only an HTTP-cache decision.

Open connections and unstable work. Open WebSockets, WebRTC, unfinished IndexedDB transactions, and some in-flight fetches can keep a page out of bfcache or get it evicted. Settling that work on pagehide, then restoring on pageshow when persisted is true, is the pattern that keeps live features and eligibility aligned.

Iframes and openers. An iframe that uses blocking APIs can disqualify the top-level page. Links that open with target="_blank" without a proper opener policy can create relationships that block caching; rel="noopener" on external new-tab links is the usual fix. Cross-origin iframe reasons may appear as masked in field APIs for privacy.

Third-party tags are frequent sources of unload and long-lived connections. The same inventory habit used for INP work in Defer, Async, and Module Scripts: A Field Guide for INP and TBT and Third-Party Scripts and Performance: The Worst Offenders applies here: name the vendor, reproduce with the tag removed, then fix or replace before rewriting first-party theme code that was never the blocker.

How to test bfcache in DevTools and Lighthouse

A cold Performance panel alone does not prove history eligibility. Chrome exposes a dedicated tester under DevTools → Application → Back/forward cache → Test back/forward cache. Chrome navigates away and back, then lists reasons under actionable, pending support, and not actionable. A clean profile or Incognito run keeps extension-injected unload handlers out of the report.

Lighthouse includes a Back/forward cache (bf-cache) audit on standalone navigations. After the main load, Lighthouse navigates away and attempts a history restore, then fails the audit when the page could not be restored, listing frames and reasons. In user-flow mode Lighthouse does not always force that end-of-navigation probe the same way; a deliberate check usually means the DevTools tester or a timespan that includes goBack(). A failing “page prevented back/forward cache restoration” finding on homepage, pricing, or checkout is a fix ticket, not a cosmetic Lighthouse warning.

After each fix class (unload removed, HTML Cache-Control adjusted, sockets closed on hide), the same tester confirms whether eligibility returned. Cold-load budget runs in Watcher or PSI still catch LCP and Performance score drift; they simply do not replace the history restore check. A bfcache pass at template onboarding or after a tag-manager publish is where eligibility usually regresses.

Field reasons with notRestoredReasons

Lab tools show blockers on the machine you control. In the field, PerformanceNavigationTiming.notRestoredReasons reports whether frames were blocked from bfcache and why (for example response cache-control no-store, unload-related reasons where still emitted, related active contents). MDN and Chrome document the shape of the tree, including iframes.

Agencies without first-party RUM will not see that API inside PageSpeed Insights alone. The practical split is DevTools and Lighthouse for eligibility on the URLs already on the monitoring schedule, and notRestoredReasons (or a RUM vendor that already reports it) when the client collects PerformanceObserver data. A single Lighthouse run is not a field pass rate. Pairing eligibility work with the same URL set used for cold-load schedules keeps the weekly report comparable.

What to put on the monitoring and client report

BFCache belongs in the retainer the same way lab and field do: separate columns, separate verbs.

  • Onboarding: DevTools bfcache test on homepage, primary landing, and one commerce or lead template, with actionable reasons filed next to LCP owners.
  • Change control: Re-check after analytics, consent, chat, or CDN header changes. Tag-manager publishes are a common unload regressor.
  • Client PDF: One short line such as “Cold lab Lighthouse (scheduled) vs history restore (bfcache eligibility),” with the failing reason attached when a fix budget is needed.
  • Boundary: Scheduled PSI covers cold loads. It does not cover back-button performance. Saying that upfront stops the next ticket from reopening the methodology argument.

Apogee Watcher keeps multi-tenant cold-load schedules, budgets, and alerts across client domains. That is the lab column, including Performance and LCP regressions after deploys. DevTools, Lighthouse’s bf-cache audit, and (where available) field notRestoredReasons cover the history column. One tool does not own both clocks; the honest pack keeps them side by side.

FAQ

Is bfcache the same as browser HTTP caching?

No. HTTP caching stores responses. BFCache stores a frozen page in memory, including JavaScript state, for history navigations. A warm HTTP cache still rebuilds the page; a bfcache restore restores the snapshot. web.dev treats that gap as the reason history restores stay faster than even a well-cached reload.

Does a green Lighthouse Performance score mean bfcache works?

No. Performance score reflects the cold navigation Lighthouse just ran. The bf-cache audit is a separate check that navigates away and back. A page can pass Performance and still fail bf-cache.

How do I know a restore happened in my own code?

The pageshow event carries event.persisted. When it is true, the page was restored from bfcache. Analytics and reconnect logic usually branch on that flag.

Should I remove all Cache-Control headers to enable bfcache?

No. HTML you want eligible typically avoids no-store and uses no-cache or short freshness when the document must revalidate. Fingerprinted static assets still keep long-lived immutable caching.

Do single-page apps get bfcache on every route change?

Soft navigations inside the app are not full history document restores. Leaving the document and returning to the SPA shell can still use bfcache.

Will Watcher’s scheduled tests tell me if bfcache is blocked?

Scheduled PageSpeed Insights runs measure cold loads. DevTools or Lighthouse’s bf-cache audit (or field notRestoredReasons) covers eligibility. Watcher still fits portfolio lab budgets and alerts on those same URLs.

CTA

When a priority URL fails the bf-cache audit, unload and document-level no-store are the usual first clearances, followed by a DevTools re-test and a cold-load schedule so the next tag publish cannot regress LCP unnoticed. Start a trial if those cold loads, budgets, and alerts need to run across many client domains, or use a free domain check to baseline the lab column before the history fixes land. Leaving history navigations untested while only shipping cold-load screenshots is how retainers keep arguing about the wrong clock.

References


Internal only - do not publish

Calendar: Watcher #102 / Thu 5 Nov 2026.

Pass 0: DataForSEO IP whitelist failed; public SERP + web.dev/Chrome docs. Primary bfcache. Brief: 102-serp-brief.md.

Pass 0.5: agency_tech_lead, journey diagnose, contrast opener.

Links brief: 07, 55, 08 (+ 81, 99, third-party offenders).

Writing re-pass (4 Nov 2026): Pass 2 / 4 / 4c / 4d against ai_writing_guide — removed money URLs, surfaces, bare how-to imperatives (§2.21), “Learn why” excerpt; joined staccato beats; descriptive playbook tone.

Top comments (1)

Collapse
 
hanin_mp profile image
Hanin Mp •

Treating bfcache as a third clock next to lab and field is a useful framing, and the point about restores disappearing from load datasets explains a lot of confusing RUM averages. The no-store on authenticated HTML is the one I run into most on client sites. Quick heads up too: the section at the very bottom headed Internal only, do not publish seems to have gone out with the post, so you may want to trim it.