TL;DR, Quick Answer
8 min readThe 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.

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.
| Blocker | Effect on bfcache | Fix |
|---|---|---|
unload listener | Ineligible in Chrome and Firefox on desktop; Firefox also on mobile except iOS | Use pagehide instead |
Cache-Control: no-store on the page | Historically keeps the page out of bfcache | Use no-cache or max-age=0 unless the page is sensitive |
Unconditional beforeunload listener | No longer blocks in modern browsers but did previously, per web.dev | Add it only while unsaved changes exist |
window.opener reference | A page with a non-null opener "can't safely be put into bfcache" | Use rel="noopener" |
| Open IndexedDB connection | Some browsers will not cache the page | Close it on pagehide, reopen on pageshow |
In-progress fetch() or XMLHttpRequest | Some browsers will not cache the page | Let requests finish or abort them on pagehide |
| Open WebSocket or WebRTC connection | Blocks in some browsers; Chrome 149 and later and Safari do not block on open WebSockets | Close 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.

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
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 onpagehideandvisibilitychangeinstead. - 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.
- The tracker sends its pageview on load
- A bfcache restore fires no load event
- The back click is never counted
- pageshow also fires on every normal load
- Each load sends two pageviews
- Restores are counted, but loads are doubled
- The listener sends only when event.persisted is true
- A normal load stays at one pageview
- Each restore adds exactly one
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.
- Add the listener. web.dev's Google Analytics example sends
gtag('event', 'page_view')insideif (event.persisted), leaving the normal load to the default pageview. - Tag the navigation type. To measure your hit ratio, web.dev disables the default pageview with
send_page_view: false, sends one withnavigation_typefromperformance.getEntriesByType('navigation')[0].type, and sendsnavigation_type: 'back_forward_cache'on restores. Compareback_forwardagainstback_forward_cachecounts. - 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.
- Collect reasons from real visits. The
notRestoredReasonsproperty 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 asunload-listenerandresponse-cache-control-no-store. - Fix the blockers. Replace
unloadwithpagehide, dropno-storefrom pages that are not sensitive, and close IndexedDB or WebSocket connections onpagehide.
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
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
How SPA Tracking Counts Pageviews When the Page Never Reloads
Accurate SPA tracking means one pageview per screen with the right referrer. How the History API, virtual pageviews and GA4 history events fit together.


Why Your GA4 Bounce Rate Is Just Engagement Rate Flipped
Your GA4 bounce rate is the share of sessions that were not engaged. How Google defines it, how it differs from Universal Analytics, and where to add it.


What the GA4 Landing Page Report Counts and Where It Goes Blank
Google's GA4 landing page report lists the first page of every session. Where to find it, its default metrics, why (not set) rows appear and how to add source.


How GA4 Site Search Tracking Records What Visitors Type
Set up GA4 site search with enhanced measurement, add your own query parameters, find the Search term dimension and track searches without a URL.


Setting GA4 User Properties and Reporting on Them
Set GA4 user properties with gtag or Tag Manager, register them as user-scoped custom dimensions and stay inside the 25-property limit.


How to Link Search Console to GA4 and Publish Its Reports
Learn how to link Search Console to GA4 in Admin, which roles you need, how to publish the two reports in Library and what the data can't join.
Related Articles


What Became of Chrome's Third Party Cookie Deprecation
Chrome's third party cookie deprecation ran from a 2020 plan to a 2025 reversal. Every Google announcement, what Chrome does now, and what it means.


Why CNAME Cloaking No Longer Hides Trackers From Safari or Brave
Trackers used CNAME cloaking to pose as your subdomain. See how the DNS trick works, why Safari caps its cookies at 7 days and how Brave and uBlock uncloak it.


How Cookie Syncing Matches Ad IDs Across Websites
Ad-tech firms use cookie syncing to swap user IDs through redirects and pixels. See what Safari, Firefox and Chrome do about it and why analytics skips it.

