GTM now counts bfcache restores
On November 9, 2026, the Google Tag Manager release notes gained a short entry about the back/forward cache. In plain terms: web measurement tags that already record a page view on history change will now also record one when the browser restores a page from the bfcache.
Contents
One sentence in the release notes
On November 9, 2026, the Google Tag Manager release notes gained a short entry about the back/forward cache. In plain terms: web measurement tags that already record a page view on history change will now also record one when the browser restores a page from the bfcache.
It looks like housekeeping, at first. Finally restored pages are treated like any other back or forward navigation, everyone moves on.
I had to read it twice, because it joins two things that have nothing to do with each other at the browser level. A history change is something a page does to itself. A bfcache restore is something the browser does to a page. One is the daily business of single page applications. The other, by definition, only happens when you leave a document and come back to it.
So why does the same switch control both? And what does that choice mean for the people who turned the switch off on purpose?
This post walks through what the bfcache is, why it has been a quiet hole in page view counts for years, why Google most likely wired the fix to this particular setting, and who ends up better or worse off. A warning up front: the release note is three sentences long. Several things below are my reading of it, and I flag them as such. The last section lists what still needs a test.
What the bfcache actually is
The back/forward cache is a browser optimisation for one specific moment: the user presses back or forward. Instead of destroying a page when the user leaves it, the browser keeps the whole thing in memory, pauses its JavaScript, and puts it aside. If the user comes back, the browser shows the page again and resumes execution. No network request, no HTML parsing, no scripts running from the top.
The word “cache” undersells it. The HTTP cache stores responses. The bfcache stores a live page: the DOM, the JavaScript heap, scroll position, form state, pending timers. What comes back is the same page instance that was frozen, not a fresh copy of it.
This is not a niche case. According to web.dev, Chrome usage data puts back and forward navigations at roughly 1 in 10 on desktop and 1 in 5 on mobile. Firefox and Safari have had a bfcache for years, and Chrome has shipped one since version 96.
Two details matter for measurement.
First, a restore is observable. The pageshow event fires on a normal load and on a restore, and its persisted property is true only for the restore. That flag is the whole detection mechanism.
Second, not every page qualifies. The classic disqualifier is an unload listener, which makes a page ineligible in desktop Chrome and Firefox. Open connections and certain cache headers can also get in the way. So the share of your back navigations that are bfcache restores depends on your own code, and on every third-party script you load.
The blind spot
Think about what a tag manager depends on. The container snippet runs when the document is parsed. It loads the container, the container pushes its initial events, the page view trigger fires, the tags go out. Every step hangs off script execution during a page load.
A bfcache restore has no page load. Nothing is parsed, so the snippet does not run again. The container is already in memory, paused mid-life, and it wakes up exactly where it stopped. From its point of view no time has passed and nothing has happened. No container load event, no DOM ready, no window loaded. The page view trigger has nothing to react to.
The user, meanwhile, is looking at a page. By any reasonable definition that is a page view. It just never reached your data.
The awkward part is that the better your site performs, the more views you lose. Remove the unload listeners, clean up the third parties, make more pages bfcache-eligible, and more of your back navigations become invisible restores. A site that does everything right for Core Web Vitals could be undercounting compared with a slower site that reloads on every back press.
This has been known for a while. The web.dev article on the bfcache has long recommended sending a page view from a pageshow listener when persisted is true. In practice, few implementations did.
This is the kind of gap that does not show up as an error, only as a number that it looks normal if you never A/B tested the implementations.
History change is a different animal
Now the other half of the release note.
A history change is a same-document navigation. The page calls history.pushState() or replaceState(), or the user presses back and the browser fires the eventpopstate. The URL changes, the application swaps its content, and the document stays exactly where it is. This is how single page applications move between routes.
Tags that record a page view on history change listen for those events. In GA4 this is the Enhanced Measurement option for page changes based on browser history events. It exists because an SPA produces one real page load and then a long series of route changes that would otherwise count for nothing.
Let’s the two mechanisms side by side, shall we:
| History change | bfcache restore | |
|---|---|---|
| Navigation type | Same document | Cross document |
| Who triggers it | The page’s own code, or back/forward within the app | The browser, on back/forward to a previous document |
| Signal | pushState, replaceState, popstate |
pageshow with persisted === true |
| Document reloaded | No | No |
| Scripts re-run | No | No |
| Typical home | Single page applications | Any eligible page, mostly multi-page sites |
The first three rows are why the coupling looks odd. Different trigger, different event, different kind of site.
The next two rows are why it is less odd than it looks. In both cases the user sees a new page and the document never loaded. That is the one thing they share, and it is the thing a page view tag cares about.
web.dev is explicit that the bfcache does not apply to the soft navigations inside an SPA. Those are the application’s business. The bfcache only deals with navigations the browser itself manages.
One switch, two mechanisms
My first reaction was that this makes no sense. If I have page views on history change enabled, I probably run an SPA. The bfcache is mostly a multi-page concern. Why would the fix for one live behind the setting for the other?
Then I realized that the history option in Enhanced Measurement is on by default. Most properties never touch it. That means a very large number of ordinary multi-page sites have it enabled without being SPAs and without anyone having decided anything. On those sites it has done nothing for years, because nothing ever calls the History API.
Seen that way, the choice is pragmatic. Google needed a place to attach bfcache support. A new setting would have meant a new control, new documentation, and a default to argue about. An existing setting that is already on almost everywhere gives near-universal rollout with no UI change at all. I am inferring the motive, but the mechanics point that way.
There is a semantic argument too, if you read the setting generously. It stops meaning “listen to the History API” and starts meaning “count page views that happen without a document load”. Under that definition a bfcache restore belongs there. The release note leans on the same idea when it frames the change as bringing restored pages in line with how these tags already treat back and forward navigation.
I can follow the logic and still dislike the result. One switch now controls two behaviours with different causes and different audiences. You cannot have one without the other. And the name of the setting no longer describes what it does.
Who gains, who doesn’t
The coupling sorts implementations into groups, and the sorting is a little ironic.
Multi-page sites on defaults. These are the winners. The option is on, nobody chose it, and restored pages now start producing page views. Expect back and forward traffic to show up where it was missing. Expect page views per session to rise somewhat, with no change in user behaviour behind it. The size of the shift depends on how many of your pages are bfcache-eligible and how much of your traffic is mobile.
SPAs on defaults. These rely on the automatic history listener for route changes. They gain coverage for the case where the whole application is restored, which the next section covers.
Carefully built SPAs. This is the group that loses out. On a serious implementation the history option is usually switched off. The automatic listener fires at the moment the URL changes, which is often before the new title, the new content and the new data layer values exist. So the team disables it and sends virtual page views from the data layer, at the right moment, with the right parameters. That is the better design. It is also, as far as I can read the note, outside the scope of this change. No subscription to history change events, no bfcache handling.
Anyone who already solved it by hand. If you added your own pageshow listener and you also left the history option on, you may now be counting each restore twice. This one is worth checking before anything else.
So the update helps most where the least thought went in, and helps least where the tracking is most deliberate. That is not a criticism of the rollout logic. Defaults are where the volume is. But it does mean the teams most likely to read a release note are the ones it does the least for.
Where the SPA does meet the bfcache
Inside an SPA, the bfcache is irrelevant. But an SPA is still a document, and a document can be frozen.
Picture a user three routes deep in an application. They click a link to an external site, or to a part of your own domain that is served as a separate full page load. The browser may put the entire application into the bfcache, frozen on that third route. The user presses back. The application reappears on the third route, instantly.
Nothing in a normal SPA setup notices. There was no page load, so no initial page view. The router did not navigate, so no pushState and no popstate inside the app. The virtual page view logic has no reason to run. It is a page view with no event attached to it, and it is the gap this change closes for tags that are subscribed.
A frozen application also brings its state back with it, and that deserves some thought:
- The data layer is restored as it was. Every earlier push is still in the array, and every variable holds its last value. A page view sent on restore will read whatever the last route left behind.
- Time has passed for the user but not for the page. If the restore happens after the session timeout, the restored page view may open a new session, on a deep route, with stale context.
- Consent, login state and experiment assignment may have changed in another tab or on the page the user just came from.
None of this is a reason to ignore restores. It is a reason to decide on purpose what a restored page view should carry, instead of letting it inherit whatever was in memory.
What to test, what to do
Start by finding out which group you are in.
- Check the setting. In the GA4 web stream, look at the Enhanced Measurement option for page changes based on browser history events. Note whether it is on, and whether that was a decision or a default.
- Check eligibility. Chrome DevTools has a back/forward cache panel under Application that runs a test navigation and lists what blocks the page. If your key templates are not eligible, none of this affects you yet.
- Search for existing handling. Look for
pageshowandpersistedin your codebase and in custom HTML tags. If something already sends a page view on restore, you have a double-count risk. - Watch a real restore. Load a page, navigate to a second one, press back, and watch the network panel for the page view hit. Do it with the option on and off. Do it outside preview mode as well, since debugging tools can change how a page behaves.
- Annotate the date. If the option is on, mark November 2026 in your reporting so the step in page views is not later read as a traffic change.
If you run virtual page views and the option is off, you still need your own listener. The pattern is small:
window.addEventListener('pageshow', function (event) {
if (event.persisted) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'virtual_page_view',
page_restored_from_bfcache: true
});
}
});
Use whatever event name your implementation already uses for route changes, so the restore goes through the same trigger and the same tags. Keep the extra flag. Being able to separate restored views from loaded ones is useful for debugging, and for explaining the trend line to whoever asks.
Before the push, refresh anything that may be stale: page-level variables, consent state, user state. The restored data layer (and page) remembers the past, not the present.
What the note does not say
The entry is short, and it leaves real questions open. These are the ones I would want answered before treating any of the above as settled.
- Which tags? The note says web measurement tags that subscribe to page view on history change. It names no tag types. GA4 through the Google tag is the obvious reading. Whether Ads and Floodlight tags are included is not stated.
- Which switch, exactly? I have assumed the Enhanced Measurement option. The note does not name it.
- Is the restored page view marked? If it arrives as an ordinary
page_viewwith nothing to tell it apart, you cannot segment it or back it out. - Do GTM triggers change? Nothing suggests the History Change trigger or the Page View trigger now fire on a restore. If they do not, tags that hang off those triggers stay blind.
- Is there an opt-out? None is mentioned. If you want history-based page views without bfcache page views, or the reverse, there appears to be no way to say so.
- Is it rolled out everywhere? The entry carries a date and nothing about staging.
The change itself is a good one. Restored pages are page views, and counting them by default fixes an undercount that most sites never knew they had.
However, the design choice is not my favourite. A setting that used to describe a mechanism now describes an outcome, and the people who had opted out of the mechanism for good reasons are opted out of the fix as well. If you are in that group, nothing has changed for you except that the gap is now easier to explain. The pageshow listener is still yours to write.
Sources
- Google Tag Manager release notes, entry of November 9, 2026
- Back/forward cache, web.dev
Comments
Comments are stored on my own server, not shared with anyone. How your data is handled.