Back/Forward Cache (bfcache): Making Back Navigations Instant

This topic belongs to Advanced Caching Strategies & CDN Architecture, and covers the one cache that makes navigations not merely fast but instant. When a user presses Back or Forward, a browser with an eligible page in its back/forward cache does not reload anything: it restores the complete page — DOM, JavaScript heap, scroll position — from memory, typically in well under 100ms. No network, no parsing, no rendering from scratch.

Back and forward navigations are a large share of all navigations — on mobile, roughly one in ten, and much more on browse-heavy sites such as search results, product listings and news feeds where users return to a list after viewing an item. Every one of those that is served from bfcache is effectively a perfect Core Web Vitals experience: LCP close to zero, no layout shift from loading, and an interactive page immediately. Every one that is not — because the page is ineligible — is a full reload, often slower than the first visit because caches have been evicted.

The browser decides eligibility per page, and a surprising number of common patterns make pages ineligible: unload handlers, Cache-Control: no-store on the document, open connections, unclosed IndexedDB transactions, and some third-party scripts. Most are fixable without product changes.

Back navigation with and without bfcache Comparison of what happens on a back navigation when the page is restored from bfcache versus reloaded. Back navigation with and without bfcache Restored from bfcache • Page restored from memory • No requests, no parsing, no rendering • Scroll and form state intact • Visible in well under 100ms Ineligible: full reload • HTML requested again (often revalidated) • CSS, JS parsed and executed again • Hydration, data fetches repeated • Often 1-3s on mobile

The Experience Degradation This Topic Addresses

The symptom rarely shows up in a dashboard by name. Users experience slow back navigations, lost scroll position on long lists, and filters reset after returning from a product page. In metrics, bfcache misses show up as:

  • Lower overall LCP quality — back navigations that reload count as page views with full LCP; restorations report near-instant metrics in RUM.
  • Worse engagement on browse flows — the list-detail-list pattern feels sluggish, increasing abandonment.
  • Extra server load — every ineligible back navigation is a request, often uncached for personalised pages.

Chrome reports bfcache eligibility explicitly in DevTools and through the notRestoredReasons API, which makes this one of the most diagnosable performance problems on the web.

Time to visible page on back navigation (mid-tier phone) Bar chart of back-navigation time for a product listing page restored from bfcache versus reloaded with warm and cold HTTP caches. Time to visible page on back navigation (mid-tier phone) bfcache restore 60ms Reload, warm HTTP cache 1150ms Reload, cold cache 2700ms

Prerequisites

  • Chrome DevTools → Application → Back/forward cache, which tests a page and lists blocking reasons.
  • The notRestoredReasons API (Chromium) via PerformanceNavigationTiming.notRestoredReasons, for field data.
  • RUM that records navigation type, distinguishing back_forward reloads from restorations (the pageshow event's persisted flag, or navigationType: 'back-forward-cache' in the web-vitals library).
  • An inventory of unload/beforeunload handlers and third-party scripts — both are frequent blockers.

1. Environment Setup: Test Eligibility in DevTools

Open the page, go to Application → Back/forward cache, and click "Test back/forward cache". DevTools navigates away and back, then reports whether the page was restored and, if not, why — distinguishing issues you can fix ("actionable"), ones that need support from a third party, and ones caused by browser limitations.

2. Capture a Baseline in the Field

Measure what share of back/forward navigations are restored, and why the others are not.

javascript
addEventListener('pageshow', (event) => {
  const nav = performance.getEntriesByType('navigation')[0];
  if (event.persisted) {
    report({ bfcache: 'restored' });
  } else if (nav?.type === 'back_forward') {
    report({ bfcache: 'missed', reasons: nav.notRestoredReasons ?? null });
  }
});
// trade-off: notRestoredReasons is Chromium-only and reports reasons for the
// main frame and same-origin frames in detail; cross-origin iframes are
// summarised. Use it to rank causes, not as a complete list everywhere.

The restoration rate — restored ÷ (restored + missed back/forward navigations) — is the topic's headline number. Sites that have never looked at it commonly find it under 30%; well-tuned sites reach 80–90% in Chromium.

3. Isolate the Blocking Reasons

Group field reasons by frequency. The common ones, roughly in order of how often they appear:

Common bfcache blockers and fixes The most common reasons pages are not restored from bfcache, whether they are under the site's control, and the fix for each. Common bfcache blockers and fixes Reason Fixable by you? Fix unload handler yes Use pagehide or visibilitychange Cache-Control: no-store yes, with care Use no-cache + private where possible Open WebSocket / WebRTC yes Close on pagehide; reopen on pageshow Unfinished IndexedDB transaction yes Commit or close before pagehide Third-party iframe or script via vendor Update, replace or delay the vendor

4. Apply the Fixes

  1. Remove unload handlers — your own and third parties' — as described in replacing unload handlers for bfcache.
  2. Reconsider no-store on HTML — see Cache-Control no-store and bfcache.
  3. Close connections on pagehide and reopen them on pageshow when persisted is true.
  4. Restore freshness on return — update prices, stock or session state when a page is restored, covered in restoring state on pageshow after bfcache.

The step-by-step resolution for each blocker is in fixing bfcache eligibility blockers.

Deconstructing a Back Navigation

A back navigation can take one of three paths, each with very different cost:

PathWhat happensTypical time (mobile)Metrics reported
bfcache restorepage resumed from memory< 100msnear-zero LCP; CLS/INP continue from restoration
HTTP-cache reloadHTML revalidated or served from cache; subresources mostly cached0.8–1.5sfull LCP; full startup work
Network reloadHTML and some subresources fetched1.5–3s+full LCP; full startup work

Which path will a back navigation take? Decision sequence for how the browser serves a back navigation, from bfcache restore to network reload. Which path will a back navigation take? Page eligible and still in bfcache? Restore from memory — instant yes no HTML cacheable and still in HTTP cache? Reload from HTTP cache — full startup work yes no Subresources cached? Network for HTML, cache for assets yes no Full network reload

The browser also evicts pages from bfcache after a time limit (minutes, varying by browser) and under memory pressure, so even eligible pages are not always restored. That is outside your control; eligibility is not.

Advanced Diagnostics and Edge Cases

Single-page apps. In-app back navigations within an SPA are not bfcache navigations at all — the router handles them. bfcache matters when the user leaves the SPA (to another site or a server-rendered page) and comes back.

Pages with Cache-Control: no-store for authentication reasons. Chrome has been relaxing this restriction in some circumstances, but you should not rely on it; separate sensitive pages from general ones.

Cross-origin iframes. An ad or embed iframe that holds an unload handler or open connection can block the whole page. notRestoredReasons lists blocked frames, sometimes masked for cross-origin frames.

Broadcast channels and shared workers. Open BroadcastChannel objects historically blocked bfcache; browser behaviour has improved, but closing channels on pagehide remains the safe pattern.

Safari and Firefox. Both have long had bfcache implementations with their own rules; the pageshow/persisted pattern works in all three engines, while notRestoredReasons is Chromium-only.

Validation and Budgeting

Track the restoration rate per template in RUM and set a target (for example 75% of back/forward navigations in Chromium). Add a Puppeteer or Playwright check for key templates: navigate to the page, navigate away, go back, and assert that pageshow reported persisted: true.

javascript
// playwright — assert a template is bfcache-eligible.
await page.goto('https://staging.example.com/search?q=boots');
await page.evaluate(() => addEventListener('pageshow', (e) => { window.__restored = e.persisted; }));
await page.goto('https://staging.example.com/about');
await page.goBack();
expect(await page.evaluate(() => window.__restored)).toBe(true);
// trade-off: headless and automated browsers may apply different bfcache
// limits; treat a failure as a prompt to check DevTools' bfcache panel.

Measuring bfcache hit rate in RUM covers the field side in depth.

Framework and Library Considerations

React, Vue and other SPAs. Frameworks themselves do not block bfcache, but common companions do: analytics libraries that flush on unload, real-time data clients that keep WebSockets open, and state-persistence plugins that write to IndexedDB in beforeunload. Audit these first; they are usually configurable.

Next.js and Nuxt. Server-rendered pages from both frameworks are generally eligible. Watch for middleware or route handlers that set Cache-Control: no-store on every HTML response by default — a common setting in authenticated apps that also affects public pages if applied globally.

Service workers. A registered service worker does not prevent restoration. Pages controlled by a service worker are restored from bfcache like any other, without the service worker being involved.

window.opener relationships. Pages opened with window.open() that keep a reference to their opener can be ineligible in some browsers. Use rel="noopener" on links that open new windows unless the relationship is required.

Cache-Control: no-cache vs no-store. Only no-store affects eligibility in the cases browsers restrict; no-cache (revalidate before use) does not. Teams frequently use no-store when no-cache would serve their purpose.

Third-Party Scripts: The Hidden Blockers

On sites that have cleaned up their own code, the remaining blockers are usually third parties. Older analytics snippets register unload handlers to flush data; chat widgets keep WebSockets open; some ad iframes hold connections or unload handlers of their own. notRestoredReasons identifies blocking frames, and DevTools often names the script.

The options mirror any third-party performance problem: update to a vendor version that uses pagehide or visibilitychange (most major vendors have shipped such fixes), configure the vendor to avoid the blocking API, load the vendor only on pages where it is needed, or replace it. Chrome has also been working towards deprecating unload entirely, with a Permissions-Policy (unload=()) that lets a site disable unload handlers for itself and its iframes — a blunt but effective tool once you have confirmed nothing you rely on needs unload.

http
Permissions-Policy: unload=()

The trade-off: any script — yours or a vendor's — that relied on unload to send data will stop doing so. Make sure analytics use visibilitychange/pagehide with sendBeacon before setting the policy.

Worked Example: A Listing-Detail Flow

A marketplace's search results page had a bfcache restoration rate of 12% in Chromium field data. notRestoredReasons showed three dominant causes: an unload handler from an old analytics snippet (61% of misses), a WebSocket for live price updates (22%), and Cache-Control: no-store on the HTML (17%, set globally by an authentication middleware). The team removed the analytics unload in favour of visibilitychange + sendBeacon, closed the WebSocket on pagehide and reopened it on pageshow, and limited no-store to account pages. The restoration rate rose to 84%. Back navigations from product pages to results became effectively instant, scroll position and filters were preserved, and the share of sessions viewing three or more products increased by 9%.

Worked example — bfcache restoration rate by fix Bar chart of the share of back/forward navigations restored from bfcache as each blocker was removed on a search results page. Worked example — bfcache restoration rate by fix Baseline 12% unload handler removed 57% + WebSocket closed on pagehide 74% + no-store limited to account pages 84%

A Rollout Plan for Improving Restoration Rate

Improving bfcache eligibility is low-risk, but it touches analytics, real-time features and security headers, so roll it out deliberately.

  1. Instrument first. Ship the pageshow beacon with notRestoredReasons for a week to get a ranked list of blockers per template. Without it, you will fix the reasons DevTools shows on your machine rather than the ones that matter in the field.
  2. Fix first-party blockers. Replace unload handlers, close connections on pagehide, commit IndexedDB transactions promptly. These are code changes your team controls entirely.
  3. Review headers. Identify which templates genuinely need no-store. Changing headers on authenticated pages needs a security review; changing them on public pages usually does not.
  4. Tackle third parties. Update vendor snippets, then consider the unload Permissions-Policy once nothing depends on it.
  5. Add freshness on restore. Before announcing the change, make sure restored pages refresh anything time-sensitive, so instant back navigations never show a stale cart or expired session.
  6. Lock it in. Add the eligibility assertion to end-to-end tests for key templates and a restoration-rate alert to the RUM dashboard, so a new vendor or middleware change cannot silently undo the work.

Most teams complete steps 1–3 in a sprint and see the largest gains there; third-party work takes longer because it depends on vendors.

Why bfcache Belongs in a Caching Strategy

bfcache is often discussed as a browser feature rather than a caching decision, but it interacts directly with the HTTP caching choices in the rest of this section. Headers you set for correctness — no-store on HTML, short max-age — determine both whether the page is bfcache-eligible and how expensive a fallback reload will be. Service workers do not interfere with bfcache restorations, but they shape the reload path. Edge-cached HTML makes reloads cheaper. Thinking about the three paths together — bfcache, HTTP cache, network — produces better decisions than tuning any of them alone, particularly for browse-heavy templates where back navigations are frequent.

FAQ

Does bfcache affect Core Web Vitals reporting?

Yes. Chrome reports bfcache restorations as separate page views in CrUX, with near-instant LCP, which improves your field distribution. The web-vitals library reports metrics for restored pages too (with navigationType: 'back-forward-cache'). More restorations means a better p75.

Is it safe to restore pages with stale data?

Usually, with a refresh on return: restore instantly, then update anything time-sensitive (cart counts, prices, notifications) in a pageshow handler. Users get an instant page and fresh data a moment later, which beats a slow reload.

Do I need to do anything for bfcache to work?

Often nothing — many pages are eligible by default. You need to act when something on the page makes it ineligible, which is common on real-world sites because of third-party scripts and legacy unload handlers.

How is bfcache different from the HTTP cache?

The HTTP cache stores responses (bytes); bfcache stores live pages (the whole document state in memory). An HTTP-cache hit still requires parsing, executing and rendering; a bfcache restore skips all of it.

Do pages in bfcache keep running JavaScript?

No. Pages in bfcache are frozen: timers, promise callbacks and network activity are paused, and resume when the page is restored. That is why open connections are a problem — the browser cannot freeze them safely — and why timers may fire immediately after restoration if their deadline passed while frozen.

How long do pages stay in bfcache?

Browsers keep only a few pages and evict them after a time limit (minutes, varying by browser and device memory) or under memory pressure. Eligibility is under your control; residence time mostly is not.

Does bfcache work for pages behind a login?

It can. Authentication alone does not block restoration; what usually does is Cache-Control: no-store added to authenticated responses, plus open connections for live data. If a restored authenticated page could show data after the user has logged out in another tab, check the session on pageshow and redirect when it is no longer valid — that keeps the instant restore for the common case while protecting the rare one.

Can I see bfcache restorations in Lighthouse?

Lighthouse includes a "Page prevented back/forward cache restoration" audit that runs a navigate-away-and-back test and lists failure reasons, which makes it a convenient CI signal. It tests one lab environment, so pair it with field data from notRestoredReasons to catch blockers that only appear for some users, such as vendors loaded conditionally.

Guides in This Topic