WordPress Speculative Loading Is Firing Your Tracking Pixels on Pages Nobody Visited

· 12 min read

A client messaged me last month asking why their Meta Pixel was showing record pageviews but conversions had flatlined. Traffic up 35%, revenue flat. Their first instinct was that the pixel was broken. Their second was that they'd been hit by bot traffic.

Neither. The culprit was a feature WordPress shipped in version 6.8 that nobody had told them about: speculative loading.

What speculative loading actually does

WordPress 6.8 (released April 2025) added the Speculation Rules API to core. It injects a <script type="speculationrules"> block into every page that tells Chromium browsers to start loading linked pages before the visitor clicks on them.

There are two modes:

  • Prefetch fetches the HTML of linked pages in the background. JavaScript on those pages does not execute.
  • Prerender goes further — it renders the entire page in a hidden tab, executing all JavaScript, CSS, and network requests as if the visitor had actually navigated there.

And there are three eagerness levels:

  • Conservative triggers on pointerdown/touchstart (the user is actively clicking a link)
  • Moderate triggers on hover after ~200ms
  • Eager triggers on any slight interaction

WordPress core defaults to prefetch with conservative eagerness. That's relatively safe — it only pre-fetches HTML, and only when the user is already clicking. But if you've installed the Speculative Loading plugin (slug: speculation-rules), the defaults change to prerender with moderate eagerness. That means hovering over any internal link for 200ms causes the browser to fully render the target page in a hidden tab, running every script on it.

That's where the tracking problem starts.

Which analytics platforms are affected

When a page is prerendered, every <script> on that page executes. If one of those scripts is a tracking pixel that fires a pageview on load, you just recorded a pageview for a page the visitor never saw.

Not affected (prerender-aware):

  • Google Analytics 4 via gtag.js — automatically defers tracking until the page is activated
  • Google Publisher Tag / AdSense — same behaviour

Affected (fire immediately, no prerender awareness):

  • Meta Pixel (Facebook) — fbq('track', 'PageView') fires on script load
  • TikTok Pixel — fires on script load
  • LinkedIn Insight Tag — fires on script load
  • Bing UET — fires on script load
  • Pinterest Tag — fires on script load
  • Microsoft Clarity — not prerender-aware
  • Any custom analytics or conversion tracking loaded via standard <script> tags

If you're running Meta ads and your WordPress site has speculative loading with prerender enabled, your Events Manager is counting phantom pageviews. Worse, if a conversion event (like a thank-you page) gets prerendered, you're recording phantom conversions too. Meta's ad algorithm optimises against that inflated data, which degrades your ROAS silently.

GA4 via GTM is a grey area. Direct gtag.js handles prerendering correctly. But if you load GA4 through Google Tag Manager, GTM itself doesn't defer tag firing during prerender — it depends on how the tags and triggers are configured. Most GTM setups I see in the wild will fire during prerender.

How to tell if you're affected

The inflation only happens in Chromium browsers (Chrome, Edge, Opera) version 121 and later. Safari and Firefox ignore speculation rules entirely. So the first clue is a skew toward Chrome in your analytics.

Check your current speculation rules by viewing source on any page and searching for speculationrules. You'll see something like:

{
  "prefetch": [{
    "source": "document",
    "where": { "and": [
      { "href_matches": "/*" },
      { "not": { "href_matches": ["/wp-*.php", "/wp-admin/*"] }}
    ]},
    "eagerness": "conservative"
  }]
}

If the key says prefetch, JavaScript on target pages does not execute, and your tracking pixels are safe. If it says prerender, they're firing.

To check programmatically whether a page was prerendered, open the browser console on any page and run:

performance.getEntriesByType('navigation')[0].activationStart > 0

If that returns true, the page was prerendered before you arrived.

In your analytics, look for pageviews with zero engaged time, zero scroll depth, and no subsequent events. Those are phantom views from prerendered pages that the user never activated.

Fix 1: Switch from prerender to prefetch

If you're running the Speculative Loading plugin, it defaults to prerender mode. You can change it under Settings > Reading — set the mode to Prefetch. Prefetch still gives you the performance benefit (faster navigations) without executing JavaScript on target pages.

If you're on WordPress core without the plugin, you're already on prefetch by default. But if you've customised it via code, verify:

add_filter( 'wp_speculation_rules_configuration', function ( $config ) {
    if ( is_array( $config ) ) {
        $config['mode'] = 'prefetch';
        $config['eagerness'] = 'conservative';
    }
    return $config;
} );

This is the lowest-effort fix and preserves the performance benefit.

Fix 2: Exclude sensitive pages from speculative loading

Even in prefetch mode, you may want to exclude checkout, thank-you, and conversion pages from speculative loading entirely. WordPress provides a filter for this:

add_filter( 'wp_speculation_rules_href_exclude_paths', function ( $paths ) {
    $paths[] = '/checkout/*';
    $paths[] = '/order-received/*';
    $paths[] = '/thank-you/*';
    $paths[] = '/my-account/*';
    return $paths;
} );

WordPress already excludes /wp-admin/*, /wp-*.php, uploads, plugins, and any URL with a nonce parameter. Links with the CSS class no-prefetch or no-prerender are also skipped automatically.

If you need mode-specific exclusions (exclude from prerender but allow prefetch), the filter passes the mode as a second parameter:

add_filter( 'wp_speculation_rules_href_exclude_paths', function ( $paths, $mode ) {
    if ( 'prerender' === $mode ) {
        $paths[] = '/shop/*';
    }
    return $paths;
}, 10, 2 );

Fix 3: Guard your tracking scripts against prerender

If you want to keep prerender mode for the performance gains but stop tracking pixels from firing prematurely, wrap your pixel initialisation in a prerender check. The browser exposes document.prerendering for exactly this purpose:

function initTracking() {
    fbq('init', 'YOUR_PIXEL_ID');
    fbq('track', 'PageView');
}

if (document.prerendering) {
    document.addEventListener('prerenderingchange', initTracking, { once: true });
} else {
    initTracking();
}

The prerenderingchange event fires once when the user actually navigates to the prerendered page (it becomes "activated"). Until that happens, the pixel stays dormant.

For GTM, wrap the entire container initialisation:

function loadGTM() {
    (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
    new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
    j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
    'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
    })(window,document,'script','dataLayer','GTM-XXXXXXX');
}

if (document.prerendering) {
    document.addEventListener('prerenderingchange', loadGTM, { once: true });
} else {
    loadGTM();
}

This ensures no GTM tags fire until the visitor genuinely arrives on the page.

Fix 4: Disable speculative loading entirely

If none of the above fits your situation — maybe you have a complex custom tracking setup or a third-party plugin that injects scripts you can't easily wrap — you can disable speculative loading completely with one line:

add_filter( 'wp_speculation_rules_configuration', '__return_null' );

Drop that into your theme's functions.php or a site-specific plugin. It prevents the <script type="speculationrules"> block from being generated at all. No speculation rules means no prefetching and no prerendering.

You lose the performance benefit, but for some sites the tracking accuracy matters more.

Which fix should you use

For most sites I maintain, Fix 1 (switch to prefetch) plus Fix 2 (exclude checkout and conversion pages) is the right balance. You keep the speed improvement — prefetch cuts perceived navigation time noticeably — without inflating analytics.

Fix 3 (JavaScript guards) is the right approach if you specifically want prerender mode and you control all your tracking scripts. It doesn't work well if plugins inject their own tracking code that you can't modify.

Fix 4 (disable entirely) is the nuclear option. I've used it on one client site where a third-party booking plugin injected conversion tracking via inline scripts that I couldn't wrap without forking the plugin.

How I caught this on a client site

The client in question had the Speculative Loading plugin installed — their previous developer had added it for performance. The plugin defaults to prerender + moderate eagerness, which is significantly more aggressive than core's defaults. Every time a visitor hovered over a navigation link for 200ms, Chrome prerendered the target page and fired the Meta Pixel.

Their homepage linked to eight category pages. A single visitor hovering across the nav generated up to nine Meta Pixel pageviews. Multiply that by a few hundred daily visitors and the numbers inflated fast.

The fix was three lines of PHP: switch mode to prefetch, leave eagerness on conservative, and exclude checkout paths. Pageview counts in Meta Events Manager dropped by about 40% overnight — back to where they should have been. Their ad performance stabilised within a week as Meta's algorithm recalibrated against accurate data.

What to check on your site right now

  1. View source on your homepage. Search for speculationrules. Note whether it says prefetch or prerender.
  2. Check if the Speculative Loading plugin is installed. If it is, check your settings under Settings > Reading.
  3. Compare your Meta Pixel pageview counts against GA4. If Meta is significantly higher and the gap appeared around the time you updated to WordPress 6.8 or installed the speculation plugin, speculative loading is likely the cause.
  4. If you're running prerender mode, apply one of the four fixes above.

This is one of those issues that costs real money — inflated ad data leads to wasted ad spend — and it's completely silent. No errors in the console, no warnings in WordPress, no indication that anything is wrong until you notice the numbers don't add up.


Stop Firefighting. Start Maintaining.

I manage 70+ WordPress sites for UK agencies and businesses. Whether you need ongoing WooCommerce maintenance, emergency support, or a one-off performance fix — I can help.

View Maintenance Plans → Get in Touch →

Stop Firefighting. Start Maintaining.

I manage 70+ WordPress sites for agencies and businesses. Whether you need ongoing maintenance, emergency support, or a one-off performance fix — I can help.

View Maintenance Plans Get in Touch

Get in Touch to Discuss Your Needs