How the Back-Forward Cache Makes the Back Button Feel Instant
Press the back button on a well-built site and the previous page appears in roughly the time it takes to paint a frame.

Press the back button on a well-built site and the previous page appears in roughly the time it takes to paint a frame. No spinner, no layout jumping into place, the scroll position exactly where you left it, and a half-filled form still half-filled. That is not the HTTP cache at work, and it is not a service worker either. It is the back-forward cache, usually shortened to bfcache, and it is one of the cheapest performance wins a front-end team can collect.
Chrome's own field data has suggested that around one in ten desktop navigations and close to one in five on mobile are back or forward moves. When those navigations are served from bfcache they are close to instant. When a page is ineligible, the browser rebuilds it from scratch, and users feel that difference immediately. This guide explains what the cache stores, what quietly disqualifies a page, and how to check your own site in a few minutes.
What the Back-Forward Cache Actually Stores
The ordinary HTTP cache keeps responses: HTML documents, scripts, images, stylesheets. Reusing them still means parsing HTML, running JavaScript, building the DOM and laying everything out again.
Bfcache works at a different level. When you leave a page, the browser freezes the whole live document in memory: the DOM tree, the JavaScript heap, pending timers, scroll offsets and form state. Execution is paused. If you come back, the browser thaws that snapshot and resumes it. Nothing is fetched or re-executed, which is why the result feels instant rather than merely fast.
Safari and Firefox have had versions of this for many years. Chrome shipped it broadly on desktop and Android in 2021, and each browser has since widened the set of pages it will accept.
The Lifecycle Events You Need to Know
Because a restored page never reloads, the load event does not fire again. Code that assumes every visit starts with a fresh load can show stale data. Two events cover the gap.
pagehidefires when the user leaves. Itspersistedproperty is true if the page may go into bfcache.pageshowfires when the page is displayed, including on restore. Again,persistedtells you whether it came from bfcache.
A common pattern is to listen for pageshow and, when event.persisted is true, refresh only the parts that can go stale, such as a cart count, a notification badge or a "last updated" timestamp. Re-rendering the whole page defeats the purpose.
Common Reasons a Page Gets Rejected
Browsers refuse to cache pages when restoring them could break something or leak data. Most rejections come from a short list.
- An
unloadevent listener. This is the biggest single blocker. Firefox treats any unload handler as disqualifying, and Chrome is phasing the event out entirely. Analytics snippets and older libraries often add one without anyone on the team knowing. Cache-Control: no-storeon the main document. Historically this blocked bfcache in every engine. Chrome has been experimenting with relaxing the rule for pages where no sensitive cookies have changed, but you should not rely on it yet.- Open connections. An active WebSocket, WebRTC session or IndexedDB transaction at the moment of leaving can make a page ineligible. Closing them in
pagehideand reopening inpageshowusually fixes it. - Pages opened with
window.openerreferences. A page that another window still holds a scripting reference to may be skipped. Usingrel="noopener"on links that open new tabs helps. - In-progress fetches or broadcast channels in some browsers, depending on version.
How to Test Your Own Pages
Chrome DevTools has a dedicated panel. Open Application, then Back/forward cache under Background services, and click "Test back/forward cache". DevTools navigates away and back, then lists either success or the exact reasons for failure, split into ones you can fix, ones pending browser support and ones outside your control.
For a quick manual check in any browser, add this to the console before navigating away:
addEventListener('pageshow', e => console.log('restored from bfcache:', e.persisted))
Then click a link, press back and read the log. Remember to check with the console set to preserve logs. Testing across engines matters here, because Safari and Firefox apply different rules. Our article on why a site works in Chrome but breaks in Safari explains why those differences show up in the first place.
Measuring It in Production
Lab tests show eligibility for one page in one state. Real users hit pages with different scripts loaded, different consent choices and different extensions. To measure the hit rate, record a beacon in pageshow with the persisted value and compare it with total back navigations, which you can identify through the Navigation Timing API's type value of back_forward.
Chrome also exposes a notRestoredReasons property on navigation timing entries. It reports, in the field, why a specific page was not restored, including blockers inside cross-origin iframes. Teams that start logging it are often surprised to find a single third-party chat widget responsible for most failures. Fixing or lazy-loading that one widget can move a hit rate from under 30 percent to well over 70 percent, which shows up directly in Core Web Vitals reports.
Fixes Ranked by Effort
| Blocker | Typical fix | Effort |
|---|---|---|
| Your own unload handler | Replace with pagehide or visibilitychange | Low |
| Third-party unload handler | Update the library or apply a Permissions-Policy header disallowing unload | Low to medium |
| Open WebSocket | Close on pagehide, reconnect on pageshow | Medium |
| no-store on public pages | Use no-cache or a short max-age instead | Medium |
| Stale UI after restore | Refresh specific widgets when persisted is true | Medium |
The Permissions-Policy route deserves a mention. Sending Permissions-Policy: unload=() stops any script on the page, including third-party ones, from registering unload handlers in Chrome. It is a blunt tool, so check that nothing important depended on unload before turning it on.
Frequently Asked Questions
Is bfcache a privacy risk for logged-in pages?
Browsers account for this. If a user logs out in another tab, or cookies change in sensitive ways, engines can evict cached pages. For pages showing private data, keep no-store and accept the trade-off, or verify session state in pageshow and redirect if it has expired.
Does bfcache work with single-page applications?
Only for navigations that leave the document. Route changes inside an SPA use the History API and never touch bfcache. If someone navigates from your SPA to another site and comes back, though, the whole app can be restored from the cache.
How long does a page stay in the cache?
Not long. Chrome typically keeps pages for a few minutes and evicts them sooner under memory pressure, especially on low-end Android phones. The cache is designed for the quick back-and-forth people do while browsing, not for returning hours later.
The Takeaway
Remove unload handlers, avoid no-store where it is not needed, close live connections on pagehide and refresh small pieces of state on pageshow. Run the DevTools test on your top ten templates, then log the field data. Few changes give users such a visible speed boost for so little code. More guides on browser behaviour live in our browsers section.
More in Browsers
Browsers
What Render Blocking Resources Are and How to Fix Them
Your page has a fast server, compressed images and a tidy bundle, yet users still stare at a white screen for a second and a...
Browsers
Why a Site Works in Chrome but Breaks in Safari
The bug report says "the booking form is broken", and on your machine it works perfectly.
Browsers
How Browsers Decide Which Cookies to Send With a Request
Most developers set a cookie, watch it appear in storage, and assume it rides along on every request after that. It does not.