⚡ Web performance

Back/forward cache: instant pages with the Back and Forward buttons

How the browser's bfcache works, which coding habits block it and how to check that your site's pages are restored instantly when users go back.

A good share of navigations on a website happen through the Back and Forward buttons: you open a product page, go back to the list, open another one. If every return forces the browser to download, parse and execute the page again, the user pays the full cost of loading each time. The back/forward cache, or bfcache, removes that cost: it keeps the whole page in memory, JavaScript state included, and restores it in a few milliseconds.

It is one of the optimizations with the best ratio between effort and result, because it requires no new code: you simply must not prevent it.

How it works

When the user leaves a page, instead of destroying it the browser "freezes" it: it suspends timers, pauses script execution and keeps the DOM and the JavaScript heap in memory. If the user goes back, the page is "thawed" exactly as it was, with the scroll position unchanged, form fields still filled in and variables intact.

This is different from the HTTP cache. The HTTP cache avoids downloading files again, but the page still has to be rebuilt: HTML parsed, CSS applied, scripts executed. The bfcache skips all of that, which is why going back feels instant.

What prevents restoration

The browser refuses to put a page in the bfcache when it fears that freezing it might break something. The most common causes are:

  1. unload event handlers: the classic reason. The browser cannot guarantee that unload runs if the page is kept, and in several cases it prefers to exclude the page;
  2. the Cache-Control: no-store header on the HTML document: it says the page must not be stored and, depending on the browser, may exclude it from the bfcache;
  3. open connections at the moment of leaving, such as WebSocket, WebRTC or requests still in flight, which the browser does not know how to suspend;
  4. windows opened with window.opener that keep a mutual reference between pages;
  5. particular APIs in use, such as some device-access features.

Many of these blockers come from third-party scripts: a chat widget that opens a WebSocket or an analytics library that registers an unload handler can exclude every page on the site.

Replacing unload with pagehide

The unload event used to be the place to send data on exit, for example a session statistic. But it is unreliable even without considering the bfcache: on mobile devices it often does not run at all, because the system closes the tab without warning.

The right alternatives are:

  • pagehide, which fires when the page is left, whether it ends up in the bfcache or is destroyed;
  • visibilitychange with the hidden state, which also fires when the user switches to another app and is the most reliable moment to send data.
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    navigator.sendBeacon('/stats', JSON.stringify(sessionData));
  }
});

sendBeacon queues the request so that it completes even if the page is closed right afterwards.

Handling the return from the bfcache

A restored page does not receive a new load event, and this can be surprising. If your code updates something only on load, for example the number of items in the cart, after going back it might show stale data. To detect restoration, use pageshow and its persisted property:

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    refreshCart();
    trackPageView();
  }
});

Likewise, if you open long-lived connections, close them in pagehide and reopen them in pageshow: that way they do not block the bfcache and the page keeps working after the return.

Sensitive data

For pages with personal information, such as a members' area after logout, the worry is that the Back button might show data that should no longer be visible. The cleanest solution is not to forbid the bfcache but to check the authentication state in the pageshow handler and, if the session has ended, reload or redirect. This protects the user without giving up speed on the rest of the site.

Checking with tools

Chrome DevTools offers a dedicated test in the Application panel, under Back/forward cache. When you press the test button, the browser navigates away and back, then reports whether the page was restored and, if not, lists the reasons. The reasons are grouped into categories: those the developer can fix, those waiting for browser support and those that cannot be fixed.

Lighthouse also includes an audit that flags pages excluded from the bfcache. For field data, Chrome exposes the notRestoredReasons property on navigation entries, which lets you collect from real users the reasons why a page was not restored.

Symptom Likely cause Fix
Slow return on every page Script using unload Switch to pagehide or visibilitychange
Only pages with chat excluded Open WebSocket Close it in pagehide, reopen in pageshow
Dynamic pages excluded no-store on the HTML Consider no-cache or private
Stale data after going back Logic tied to load Refresh in pageshow with persisted

In short

  • The bfcache keeps the entire page in memory and restores it instantly on Back and Forward.
  • The main enemy is the unload event: replace it with pagehide or visibilitychange.
  • Use pageshow with persisted to refresh data after a restore.
  • Close open connections on exit and reopen them on return.
  • Test with DevTools and collect the reasons from real users.
MM
Marco Marzaduri

Marco Marzaduri works in cybersecurity, digital forensics and web development. A computer science graduate, he combines rigorous technical analysis with attention to the people who use technology.

Read also