WebTech1223411 logo WebTech1223411Web tech, read closely
Browsers

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.

A browser window with the back button highlighted and a frozen snapshot of the previous page behind it

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.

  • pagehide fires when the user leaves. Its persisted property is true if the page may go into bfcache.
  • pageshow fires when the page is displayed, including on restore. Again, persisted tells 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.

  1. An unload event 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.
  2. Cache-Control: no-store on 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.
  3. Open connections. An active WebSocket, WebRTC session or IndexedDB transaction at the moment of leaving can make a page ineligible. Closing them in pagehide and reopening in pageshow usually fixes it.
  4. Pages opened with window.opener references. A page that another window still holds a scripting reference to may be skipped. Using rel="noopener" on links that open new tabs helps.
  5. 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

BlockerTypical fixEffort
Your own unload handlerReplace with pagehide or visibilitychangeLow
Third-party unload handlerUpdate the library or apply a Permissions-Policy header disallowing unloadLow to medium
Open WebSocketClose on pagehide, reconnect on pageshowMedium
no-store on public pagesUse no-cache or a short max-age insteadMedium
Stale UI after restoreRefresh specific widgets when persisted is trueMedium

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.

MC
Maren Castellanos

Maren worked in cross-browser QA before editing, and knows the DevTools Network tab better than most people know their inbox. She writes about how browsers load and render pages and about the tools that show what is really happening.

More posts by Maren

More in Browsers

01doorganico 084goad 3hot8 4465456 after5internprogram ahandfulofblessings ahmadpc ahomcybersecurity allodialresources annodex antiquesandperiodcollectibles aotc asianworldkashmir beedlectrics bloggertemplatefree bluefeatheragency bodyweightsolution campaign08blog carfixadvisor catstreetcollective cbstvsports cnfanspurchasing committeeof500yearsofdignityandresistance covertopsdjs cs-kaiyuangame cuestacomunicaciontotal cxoutlet dancebetgame deskgearhub destinyvillastay detectmagazine di33 diamondsteelbuildings donmo dyanadropship emarsysfinance emimattl escapechan eskitarih evautoplay everythingweddings-orlando farmzone feedviewerapp foxstudiosscratch gamevui123 gleasongrade greetingsday hacklabasu haskell iw-kaiyun jagoanproperti jilidskripsi joltbusinessfinance juegodepokergratis juegospucca kaeru-festival latestmehndidesigns lestanzediottaviano litemagazines localcommunityhero lustrsazan mariashebets menswheels newwebstechs1 nikkit ootheday oxigenvpn plurfitness prefetial probudgetcarpetcleaning psychicmrsmartin pyaten qvantor rejoignezlogis rexzbot riselayouts ruchin rwxqsgd5 sakaltime shibiaowang shop-bot skrtips strcoding tetapatrium topselller uneplanteartificielle vipertechcr wo-kygaming yamaha-mekar yeknava yoybuysheets zenunwa yamaha-mekar jilidskripsi ankertoto ankertoto ankertoto ankertoto