The Problem
Client switched on the Speculation Rules API two weeks ago to chase a faster LCP for repeat-visit pages. Lighthouse and CrUX both moved the right way. Then the analytics team filed a ticket: GA4 page views for the marketing site had jumped 27% week-over-week with no campaign change and the same crawler split. Bounce rate also looked artificially good — sessions with one event but a "view" on a page nobody actually navigated to.
The pattern in DebugView was unmistakable. Pages that the browser had speculatively prerendered were firing page_view twice: once when the prerender activated in the background, and a second time when the user actually clicked through. On the pages a user never visited, the first page_view still landed in GA4 because the prerender had run to completion and the tag had executed inside the hidden document.
A representative session looked like this in BigQuery:
SELECT event_name, event_timestamp, params.value.string_value AS page_path
FROM events_*
WHERE user_pseudo_id = '1234567.890' AND event_date = '20260524'
ORDER BY event_timestamp;
-- page_view 10:14:02 /pricing
-- page_view 10:14:03 /pricing <-- prerender activation
-- page_view 10:14:05 /pricing <-- real navigation
-- page_view 10:14:08 /features <-- prerender, never visited
That third event on /features is the killer. The user never opened that page; the browser prerendered it speculatively when hovering a nav link, the tag fired inside the prerendered document, and GA4 has no idea the visit was fictional.
Why It Happens
Speculation Rules tell the browser to prerender a document in a hidden render tree before the user navigates. The document runs JavaScript, fetches resources, fires DOMContentLoaded, and — critically for analytics — runs any tag that does not check document.prerendering. When the user actually clicks the link, the prerendered document is activated and becomes the visible page; if they do not click, the prerender is discarded silently.
GA4's gtag('config', ...) and the GTM container both fire on document load by default. They do not check the prerender state. So a prerendered document sends a page_view event the moment the tag initialises, regardless of whether the user ever sees the page. Activation does not fire a second tag-init in Google's libraries, but most teams have additional page_view triggers wired into route changes or SPA navigations that fire again on activation — that is where the double-count comes from on pages users actually visit.
The Chrome team documented this and added an event for it. The prerenderingchange event on document fires when a prerendered document is activated. The fix is to defer analytics until either the prerender is activated or the document was loaded normally to begin with. The same applies to A/B test bucketing, consent reads, anything else that should run "once per real visit".
The Fix
Two changes. Block the prerender-time page_view in the gtag bootstrap, and re-fire it on prerenderingchange when activation happens.
1. Defer the initial config call. Replace the standard GA4 snippet with one that respects document.prerendering. Drop this in <head> or push it through your tag manager:
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){ dataLayer.push(arguments); }
gtag('js', new Date());
function sendPageView() {
gtag('config', 'G-XXXXXXX', { send_page_view: true });
}
if (document.prerendering) {
document.addEventListener('prerenderingchange', sendPageView, { once: true });
} else {
sendPageView();
}
</script>
Key detail: send_page_view is set to true only when the document is either already non-prerendered, or when activation has fired. If the user never activates the prerender, sendPageView never runs and GA4 stays honest.
2. Patch SPA route-change tracking on App Router or any client-side router. Single-page apps fire page_view on route change, which means a prerendered document that later activates may still emit a duplicate during hydration. Guard the route-change listener too:
'use client'
import { useEffect } from 'react'
import { usePathname, useSearchParams } from 'next/navigation'
export function GAListener() {
const pathname = usePathname()
const params = useSearchParams()
useEffect(() => {
if (typeof window === 'undefined' || !window.gtag) return
const fire = () => {
window.gtag('event', 'page_view', {
page_path: `${pathname}?${params.toString()}`,
})
}
if (document.prerendering) {
document.addEventListener('prerenderingchange', fire, { once: true })
return () => document.removeEventListener('prerenderingchange', fire)
}
fire()
}, [pathname, params])
return null
}
The listener still fires on every navigation, but the very first render of a prerendered document waits for activation. The combination of the head-script gate and the route-listener gate covers both the initial load and the SPA route-change event.
3. Verify in DebugView and BigQuery. Open DebugView with the realtime debug parameter, then trigger a prerender manually by hovering a link with rel="next" or a registered speculation rule. Confirm that no page_view lands in DebugView until you click. After 24 hours, re-run the BigQuery diagnostic above on a known test user — there should be exactly one page_view per real navigation and zero on pages that were prerendered but not visited.
If you also use GTM, set the GA4 Configuration tag's "Send a page view event when this configuration loads" to No and add a Custom HTML tag with the same prerenderingchange logic to call gtag('event', 'page_view', ...) once. Same idea, different surface.
The Lesson
Speculation Rules are a real win for LCP and INP, but the prerendered document is a fully running page, and your analytics tags do not know that. Wait for prerenderingchange before firing any "real visit" event. The GA4 numbers will come back to reality on the next data refresh, and your bounce rate stops lying.
If your GA4 numbers stopped matching reality after a performance push, that is the kind of audit I do regularly. See my services. For the GA4 cousin issue that pre-dates Speculation Rules, read GA4 double counting page views in App Router.