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.
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.
Prerequisites
- Chrome DevTools → Application → Back/forward cache, which tests a page and lists blocking reasons.
- The
notRestoredReasonsAPI (Chromium) viaPerformanceNavigationTiming.notRestoredReasons, for field data. - RUM that records navigation type, distinguishing
back_forwardreloads from restorations (thepageshowevent'spersistedflag, ornavigationType: 'back-forward-cache'in theweb-vitalslibrary). - An inventory of
unload/beforeunloadhandlers 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.
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:
4. Apply the Fixes
- Remove
unloadhandlers — your own and third parties' — as described in replacing unload handlers for bfcache. - Reconsider
no-storeon HTML — see Cache-Control no-store and bfcache. - Close connections on
pagehideand reopen them onpageshowwhenpersistedis true. - 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:
| Path | What happens | Typical time (mobile) | Metrics reported |
|---|---|---|---|
| bfcache restore | page resumed from memory | < 100ms | near-zero LCP; CLS/INP continue from restoration |
| HTTP-cache reload | HTML revalidated or served from cache; subresources mostly cached | 0.8–1.5s | full LCP; full startup work |
| Network reload | HTML and some subresources fetched | 1.5–3s+ | full LCP; full startup work |
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.
// 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.
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%.
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.
- Instrument first. Ship the
pageshowbeacon withnotRestoredReasonsfor 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. - Fix first-party blockers. Replace
unloadhandlers, close connections onpagehide, commit IndexedDB transactions promptly. These are code changes your team controls entirely. - 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. - Tackle third parties. Update vendor snippets, then consider the
unloadPermissions-Policy once nothing depends on it. - 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.
- 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
- Fixing bfcache eligibility blockers — work through the reasons DevTools reports.
- Replacing unload handlers for bfcache — the most common blocker.
- Cache-Control no-store and bfcache — when the header is necessary and when it is not.
- Measuring bfcache hit rate in RUM — the field metric for this topic.
- Restoring state on pageshow after bfcache — keeping restored pages fresh.
Related
- HTTP Cache-Control headers explained — the header choices that affect eligibility.
- Service Worker caching strategies — the reload path when bfcache misses.
- Speculative loading and prefetching — the forward-looking counterpart to instant back navigations.
- Sending web vitals with sendBeacon on visibility change — beacon timing that does not block bfcache.