Glossary

What the bfcache Does to Back Navigations and Your Pageview Counts

Taras Shynkarenko
Taras Shynkarenko
•Updated: •8 min read
What the bfcache Does to Back Navigations and Your Pageview CountsWhat the bfcache Does to Back Navigations and Your Pageview Counts

TL;DR, Quick Answer

8 min read

The bfcache (back/forward cache) keeps a full snapshot of a page in memory when the visitor leaves, so a back or forward click restores it instantly without reloading. Chrome, Firefox and Safari all have one. Because the page does not load again, an analytics script that only counts pageviews on load misses the restore. Listen for pageshow and check event.persisted to count restores, and avoid unload listeners, Cache-Control: no-store and open connections that make a page ineligible.

What is the bfcache?

The bfcache, short for back/forward cache, is a browser optimization that keeps a full snapshot of a page in memory when the visitor navigates away, so a back or forward click restores that page instantly instead of loading it again. web.dev defines it as "a browser optimization that enables instant back and forward navigation," and describes the mechanism: "instead of destroying a page when the user navigates away, we postpone destruction and pause JS execution. If the user navigates back soon, we make the page visible again and unpause JS execution."

All major browsers have one: Chrome since version 96, Firefox and Safari. Back and forward clicks are a large share of browsing. web.dev reports that "Chrome usage data shows that 1 in 10 navigations on desktop and 1 in 5 on mobile are either back or forward."

For analytics, the important part is what does not happen on a restore. No HTML is requested, no scripts run from the top, and no load event fires. Your tracker wakes up exactly where it was paused.

An open book with a bookmark holding the reader's place, like a page paused in memory and picked up where it stopped.

How is the bfcache different from the HTTP cache?

The bfcache stores the whole live page, while the HTTP cache stores individual responses. web.dev puts it this way: "bfcache is a snapshot of the entire page in memory, including the JavaScript heap, whereas the HTTP cache contains only the responses for previously made requests." A page served from the HTTP cache still loads: the browser parses the HTML, runs every script and fires load, so your tracker sends a normal pageview. A page restored from the bfcache skips all of that.

Timers and promises pause too. web.dev notes that "browsers pause any pending timers or unresolved promises for pages in bfcache" and resume them on restore, so a heartbeat or engagement timer picks up after the gap rather than counting through it.

Which pages are eligible for the bfcache?

A page is eligible for the bfcache unless something on it makes caching unsafe, and the most common blockers are under your control. The table lists the ones web.dev and Chrome's documentation call out.

BlockerEffect on bfcacheFix
unload listenerIneligible in Chrome and Firefox on desktop; Firefox also on mobile except iOSUse pagehide instead
Cache-Control: no-store on the pageHistorically keeps the page out of bfcacheUse no-cache or max-age=0 unless the page is sensitive
Unconditional beforeunload listenerNo longer blocks in modern browsers but did previously, per web.devAdd it only while unsaved changes exist
window.opener referenceA page with a non-null opener "can't safely be put into bfcache"Use rel="noopener"
Open IndexedDB connectionSome browsers will not cache the pageClose it on pagehide, reopen on pageshow
In-progress fetch() or XMLHttpRequestSome browsers will not cache the pageLet requests finish or abort them on pagehide
Open WebSocket or WebRTC connectionBlocks in some browsers; Chrome 149 and later and Safari do not block on open WebSocketsClose on pagehide

Some misses have nothing to do with your code. web.dev lists back or forward navigations that do not use the bfcache "when the user quits the browser and starts it again," "when the user duplicates a tab," and "when the user closes a tab and reopens it," and notes that browsers discard cached pages after a period to save memory. A 100% hit rate is not a realistic target.

Iframes follow the main page. An iframe is not cached separately, and an iframe that uses a blocking API can keep the whole page out of the bfcache.

How do pageshow and pagehide report the bfcache?

The pageshow and pagehide events report the bfcache through their persisted property. On pageshow, persisted is true when the page came back from the bfcache and false on a normal load. On pagehide, persisted being true means the browser intends to cache the page, though web.dev stresses that it "doesn't guarantee that a page will be cached."

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    console.log('This page was restored from the bfcache.');
  } else {
    console.log('This page was loaded normally.');
  }
});

pageshow "fires right after the load event when the page is initially loading and any time the page is restored from bfcache." That detail matters for analytics, as the next section shows. Chromium browsers also fire the Page Lifecycle events freeze and resume when a page enters and leaves the bfcache, but web.dev points out that measuring your bfcache hit rate needs pageshow.

Commuters passing through a turnstile, where anyone who walks back through unnoticed goes uncounted, like a back navigation missed by analytics.

Why does analytics miss or double count back navigations?

Analytics misses back navigations because a bfcache restore does not run the page's scripts again, so a tracker that sends its pageview on load stays silent. web.dev tells site owners they can notice "a decrease in the total number of pageviews reported as Chrome enables bfcache for more users," and adds that many sites are "already underreporting pageviews from other browsers that implement bfcache, because many popular analytics libraries don't measure bfcache restores as new pageviews."

Double counting comes from the opposite fix done carelessly. Because pageshow also fires on every normal load, a listener that sends a pageview on every pageshow without checking persisted sends two pageviews per load: one from the tracker's normal startup, one from the listener.

Other distortions follow from the same mechanism:

Flowsery
Flowsery

Start Your 14-Day Free Trial

Real-time dashboard

Goal tracking

Cookie-free tracking

  • Engagement gaps. Paused timers resume on restore, so engagement timers and heartbeats skip the time the page sat in memory.
  • Exit events that never send. A page entering the bfcache never fires unload. The beforeunload vs pagehide guide covers why exit beacons belong on pagehide and visibilitychange instead.
  • Faster pages looking slower. web.dev explains that restores replace repeat visits, which are among the fastest page loads in a dataset, so load-time data skews slower even when visitors got a faster experience.
  • Mixed counts. A back navigation that misses the bfcache does a full reload and gets counted, while one that hits it does not. The same click pattern produces different numbers depending on eligibility.

The pageviews vs sessions vs users guide shows how these gaps carry through to per-session metrics.

Three ways to handle a back click
No pageshow listener
  • The tracker sends its pageview on load
  • A bfcache restore fires no load event
  • The back click is never counted
pageshow without a check
  • pageshow also fires on every normal load
  • Each load sends two pageviews
  • Restores are counted, but loads are doubled
pageshow with persisted check
  • The listener sends only when event.persisted is true
  • A normal load stays at one pageview
  • Each restore adds exactly one
Only the persisted check counts each back click exactly once.

How do you count bfcache restores correctly?

You count bfcache restores by adding a pageshow listener that sends a pageview only when event.persisted is true, then testing that each back click produces exactly one hit.

  1. Add the listener. web.dev's Google Analytics example sends gtag('event', 'page_view') inside if (event.persisted), leaving the normal load to the default pageview.
  2. Tag the navigation type. To measure your hit ratio, web.dev disables the default pageview with send_page_view: false, sends one with navigation_type from performance.getEntriesByType('navigation')[0].type, and sends navigation_type: 'back_forward_cache' on restores. Compare back_forward against back_forward_cache counts.
  3. Run the DevTools test. In Chrome DevTools, open Application, then Back-forward Cache, and click Run Test. A pass reads "Restored from back-forward cache"; a failure lists the reasons and marks fixable ones as Actionable.
  4. Collect reasons from real visits. The notRestoredReasons property on the navigation timing entry reports why a page was not restored. Chrome's documentation says it "has shipped from Chrome 123," with reasons such as unload-listener and response-cache-control-no-store.
  5. Fix the blockers. Replace unload with pagehide, drop no-store from pages that are not sensitive, and close IndexedDB or WebSocket connections on pagehide.

Performance metrics need the same care. web.dev explains that tools like the Chrome User Experience Report treat restores as separate page visits, and the web-vitals library supports restores. On a restore, Largest Contentful Paint is measured from the pageshow timestamp to the next painted frame, while Cumulative Layout Shift and INP reset to 0.

How does the bfcache affect Flowsery data?

Flowsery's docs do not describe how its tracker treats a bfcache restore, so test it on your own site before you add any code. Flowsery's real-time analytics view shows every pageview on your dashboard the moment it happens, which makes the check quick: open a page, click a link, press back, and watch whether the restored page appears as a new pageview. Run the DevTools bfcache test first so you know the page was restored and not reloaded. If restores do not appear and you want them counted, Flowsery's npm SDK exposes a trackPageview() method you can call from a pageshow listener that checks event.persisted, which avoids the double count a listener without that check would cause.

Frequently asked questions

Do all browsers have a bfcache?

Yes, all major browsers include one. web.dev lists Chrome since version 96, Firefox and Safari. Each browser has its own eligibility rules, which is why the same page can be cached in one browser and reloaded in another.

Does a bfcache restore fire the load event?

No. A restore makes the paused page visible again and resumes its JavaScript, so load and DOMContentLoaded do not fire. The pageshow event fires, with persisted set to true.

Why did my pageviews drop after Chrome expanded the bfcache?

More back and forward clicks became restores instead of reloads, and many analytics scripts only count pageviews on load. web.dev describes this exact drop. Add a pageshow listener that sends a pageview when event.persisted is true if you want restores counted.

Does Cache-Control: no-store disable the bfcache?

On the page itself, it has historically kept pages out of the bfcache. web.dev recommends using it only for pages with sensitive information, and using no-cache or max-age=0 for pages that just need fresh content. Neither of those affects eligibility.

How do I check if a page is eligible for the bfcache?

In Chrome DevTools, go to Application, then Back-forward Cache, and click Run Test. The panel reports whether the page was restored and lists the blocking reasons if not. For real visits, read notRestoredReasons from the navigation timing entry.

Does the bfcache work inside a single-page app?

No. web.dev states that the bfcache "doesn't work for 'soft navigations' within a single-page app." It applies when a visitor leaves the app and comes back with the back button, which can restore the whole app instead of starting it from scratch if the page is eligible.

What happens to timers and heartbeats while a page sits in the bfcache?

Browsers pause pending timers and unresolved promises, then resume them on restore. A heartbeat or engagement timer picks up after the gap, so the time the page spent in memory is not counted as engagement.

Does an unload listener keep a page out of the bfcache?

Yes, in Chrome and Firefox on desktop, and in Firefox on mobile except iOS. Replace unload with pagehide and the page stays eligible.

How do I avoid double counting pageviews with a pageshow listener?

pageshow fires on every normal load as well as every restore, so send the pageview only when event.persisted is true. The tracker's normal startup already covers the regular load. Then press back on a test page and confirm each click produces exactly one hit.

Does Flowsery count bfcache restores automatically?

Flowsery's docs do not describe it, so test it on your own site. Open a page, click a link, press back and watch the real-time analytics view for a new pageview. If none appears, call trackPageview() from the npm SDK inside a pageshow listener that checks event.persisted.

Was This Article Helpful?

Let us know what you think!

See us more often in Google

One click marks Flowsery as a preferred source, so our articles sit higher in your Top Stories, AI Mode, and AI Overviews.

Before you go...

Flowsery

Flowsery

Revenue-first analytics for your website

Track every visitor, source, and conversion in real time. Simple, powerful, and cookie-free.

Real-time dashboard

Goal tracking

Cookie-free tracking

Related Glossary Terms

Related Articles