Skip to content

WordPress speculative loading: check, tune or turn it off

Check whether WordPress speculative loading is prefetching pages on your site, keep cart and account pages out of it, and decide how early browsers may start loading. You will also see how prefetches show up in logs, caches and analytics, and how to switch the feature off cleanly.

If you run a WordPress or WooCommerce site, here is how to check, tune or switch off speculative loading. That feature, on by default since WordPress 6.8, lets Chrome and Edge start fetching the next page for logged-out visitors a fraction of a second before their click finishes. You may have spotted requests in your logs that you can’t explain, or you may want page changes to feel faster. The default setup is safe for almost every site, because the browser fetches a page only when someone presses a link, so nearly every early request becomes a real visit. Keep it on, exclude the few URLs that show per-visitor data or change something when opened, and turn it off with one filter only if something breaks.

What you’ll accomplish

  • Confirm whether speculative loading is active on your site, and with which settings.
  • Watch a prefetch happen in Chrome DevTools and recognise it in your server access log by its Sec-Purpose: prefetch request header.
  • Exclude cart, checkout, account and plugin “action” URLs with a small must-use plugin.
  • Decide whether to raise the eagerness or switch to prerender, using clear conditions instead of guesswork.
  • Turn the feature off completely, in a way the Speculative Loading plugin can’t override.

Prerequisites

  • WordPress 6.8 or later. Run wp core version or check Dashboard > Updates. The wp-config.php constants in step 5 need WordPress 7.1 or later.
  • Pretty permalinks. Under Settings > Permalinks, anything other than “Plain” counts. With plain permalinks, WordPress turns the feature off by default.
  • A Chromium-based browser (Chrome, Edge or Opera) for testing. Firefox and Safari ignore the rules, so they show nothing.
  • SSH or file access to wp-content/mu-plugins/, the folder for must-use plugins (PHP files WordPress loads on every request, which can’t be deactivated from the admin screen).
  • WP-CLI, which runs WordPress tasks from the shell, is helpful but optional. Every check has a browser or curl alternative.

How it works

Speculative loading uses the Speculation Rules API, a browser feature that lets a page tell the browser which links it may load ahead of time. WordPress prints one rule as a <script type="speculationrules"> block in the page footer. Browsers that don’t support it skip the block, so nothing changes for them.

WordPress only prints the rule when two conditions are true: nobody is logged in, and the site uses pretty permalinks. So you, logged into wp-admin, never see it on your own site. A WooCommerce shopper with a cart but no account is still logged out, so they do get it.

Request path of a pressed link: the WordPress rule match, excluded paths sent as a normal click, the prefetch request with the Sec-Purpose header, a page cache or CDN answer, and the failure point where an action URL that is not excluded gets prefetched

The rule matches every link on the same site, except these, which WordPress always leaves out:

  • /wp-login.php and any other /wp-*.php file, plus everything under /wp-admin/. Log-out links go through wp-login.php, so they are never prefetched.
  • Files under the uploads, wp-content, plugins and theme folders.
  • Any URL with a query string, such as ?add-to-cart=42, when pretty permalinks are on.
  • Links with rel="nofollow". WooCommerce adds it to the add-to-cart buttons in product lists.
  • Links that carry the no-prefetch class, or sit inside an element that has it.

Two settings decide what the browser does with a matching link. The mode is either prefetch (download the HTML only) or prerender (download the page and render it in a hidden tab, running its JavaScript). The eagerness is how early the browser acts. WordPress defaults to prefetch with conservative eagerness.

EagernessWhen Chrome starts on desktopOn mobile
conservative (WordPress default)Pointer or finger pressed on the linkSame
moderatePointer rests on the link for 200 ms, or press, whichever comes firstViewport heuristics based on scrolling
eagerPointer rests on the link for 10 ms (Chrome 143 and later)Link has been in view for 50 ms

Chrome also caps these at two speculations at a time per page and drops the oldest when a third starts. That cap keeps the extra load small even at eager settings.

What your server, cache and analytics see

A prefetch is an ordinary GET request with the visitor’s cookies plus one extra header, Sec-Purpose: prefetch. A prerender sends Sec-Purpose: prefetch;prerender. When the visitor completes the click, the browser uses the response it already has. So a normal visit usually produces one line in your access log, and that line is the prefetch. Don’t filter Sec-Purpose requests out of log-based statistics: you would remove real page views along with the unused ones.

Page caches and CDNs (content delivery networks, which serve copies of your pages from servers near the visitor) treat the prefetch like any other request. A cached page costs nothing extra. An uncached one runs PHP as usual, which matters for WooCommerce guests whose cart cookie bypasses the cache. Prefetch never runs page JavaScript, so tools like Google Analytics don’t count it. Prerender does run it, which is why analytics behaviour is the main question before you switch modes. For the cache side, see WordPress page cache vs object cache vs browser cache.

Step-by-step

  1. Read the active configuration. From the site’s root folder, run:

    wp option get permalink_structure
    wp eval 'echo wp_json_encode( wp_get_speculation_rules_configuration() ), PHP_EOL;'

    Example output:

    /%postname%/
    {"mode":"prefetch","eagerness":"conservative"}

    WP-CLI runs as a logged-out visitor, so this is what anonymous visitors get. If the second line says null, the feature is off: either permalinks are plain (an empty first line) or a plugin or theme returns null from the wp_speculation_rules_configuration filter. If it says prerender and moderate, the Speculative Loading feature plugin is most likely active. On WordPress 7.1 or later, a host-level default can also cause it (see step 5).

  2. Look at the rule in the page HTML. Use a logged-out request:

    curl -s https://www.example.com/ | grep -A1 'type="speculationrules"'

    Example output:

    <script type="speculationrules">
    {"prefetch":[{"source":"document","where":{"and":[{"href_matches":"/*"},{"not":{"href_matches":["/wp-*.php","/wp-admin/*","/wp-content/uploads/*","/wp-content/*","/wp-content/plugins/*","/wp-content/themes/storefront/*","/*\\?(.+)"]}},{"not":{"selector_matches":"a[rel~=\"nofollow\"]"}},{"not":{"selector_matches":".no-prefetch, .no-prefetch a"}}]},"eagerness":"conservative"}]}

    No output means no rule on that page. If step 1 showed a configuration but curl shows nothing, you are probably looking at a page cached before WordPress 6.8. Purge the page cache and try again.

  3. Watch a prefetch in Chrome DevTools. Open the site in a private window so you are logged out. Open DevTools, go to Application, and under Background services select Speculative loads. Reload the page, because the panel only records what happens while it is open. The Rules tab lists the rule set and the Speculations tab lists matching URLs with a status. Press and hold a menu link: its status moves on from “Not triggered” once the prefetch runs. In the Network panel, select that request and check Headers. You should see Sec-Purpose: prefetch under the request headers.

  4. Exclude cart, checkout, account and action URLs. Create wp-content/mu-plugins/speculative-loading-rules.php:

    <?php
    /**
     * Plugin Name: Speculative loading exclusions
     */
    add_filter( 'wp_speculation_rules_href_exclude_paths', function ( $paths ) {
    	$paths[] = '/cart/*';
    	$paths[] = '/checkout/*';
    	$paths[] = '/my-account/*';
    	return $paths;
    } );

    These pages show data that belongs to one shopper and usually bypass the page cache, so every speculation of them runs PHP. Under the default that cost is small, because a press is almost always followed by the click. It grows once you raise the eagerness, and under prerender the checkout scripts would run before the shopper even sees the page. Excluding them up front keeps step 5 safe.

    Use the slugs your shop actually uses, listed under WooCommerce > Settings > Advanced > Page setup. Paths are relative to the site’s home page. WordPress adds the subdirectory prefix itself if the site lives in one. Add any plugin URL that changes something when opened and has no query string. A hypothetical wishlist link like /wishlist/add/42/ would need '/wishlist/*'. Then check the file and purge the page cache:

    php -l wp-content/mu-plugins/speculative-loading-rules.php

    Expected output: No syntax errors detected in wp-content/mu-plugins/speculative-loading-rules.php. To exclude a single link instead, add the CSS class no-prefetch to it. In the block editor that is Advanced > Additional CSS class(es) on a Button or Group block.

  5. Change mode or eagerness only on purpose. Stay on the default unless both of these are true: anonymous pages are served from a page cache, and step 4 is done. Then moderate is a reasonable step up. Add this to the same must-use plugin:

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

    The is_array() check keeps the feature off for logged-in users. On WordPress 7.1 or later, a host can instead set define( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS', 'moderate' ); in wp-config.php, or an environment variable with the same name. WP_SPECULATIVE_LOADING_DEFAULT_MODE works the same way for the mode. Both only change what WordPress’s “auto” value turns into, so any plugin filter still wins. Use prerender only after confirming your analytics, ads and chat widgets wait for the page to be shown. The Chrome prerender documentation covers how to check that. If the Speculative Loading feature plugin is installed, it applies its own settings from Settings > Reading (prerender and moderate by default), so change them there or deactivate it.

  6. Record the header in your access log. Default log formats don’t include Sec-Purpose. On nginx, add a format in the http block and use it in the site’s server block:

    log_format specl '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_sec_purpose"';
    access_log /var/log/nginx/access.log specl;

    On Apache, the equivalent is LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" \"%{Sec-Purpose}i\"" specl, with the site’s CustomLog line switched to specl. On cPanel the per-domain logs use a format WHM manages, so use DevTools from step 3 instead. Run nginx -t && systemctl reload nginx and expect syntax is ok and test is successful.

Validate it works

Run these three checks after any change, and purge the page cache first.

curl -s https://www.example.com/ | grep -o '"/cart/\*"'

Expected output: "/cart/*". That proves your exclusion reached the HTML visitors get.

Next, in a private Chrome window with the Speculative loads panel open, press a cart link. It should show no speculation, while a blog or product link still does.

Finally, count prefetch lines in the log (nginx format from step 6):

grep -c '"prefetch"$' /var/log/nginx/access.log

Example output: 318. Any non-zero count proves Chromium visitors are prefetching. Under conservative eagerness nearly every one of these lines is also a real page view, so a sudden jump after a plugin update points to raised eagerness.

Troubleshooting

  • No rule in the HTML. Check, in order: you tested logged in, permalinks are plain, a cached copy predates the change, or a plugin returns null from wp_speculation_rules_configuration. Step 1 tells you which.
  • Rule present, but DevTools shows nothing. Chrome skips speculation when Preload pages is off in chrome://settings/performance, which some ad blockers such as uBlock Origin also turn off. A strict Content Security Policy (a header that limits which scripts can run) without 'unsafe-inline' blocks the inline rule, and the Console shows a CSP error. Add the script’s hash to script-src, or add your nonce (the random token your policy issues for each page) through the wp_inline_script_attributes filter.
  • Many unused prefetches or extra PHP load. Something raised the eagerness. Look for the Speculative Loading plugin (curl the page and search for speculation-rules in its generator meta tag) or another plugin filtering the configuration. Go back to conservative or improve the page cache hit rate. See the WooCommerce bypass-cookie recipe for guest carts behind Cloudflare.
  • 503 responses on prefetches behind Cloudflare. Cloudflare’s Speed Brain feature adds its own conservative prefetch rule through a Speculation-Rules header. Its documentation says prefetches that don’t succeed get a 503 response. The browser drops that prefetch and loads the page normally.
  • A stale cart or a state change nobody asked for. A clean-path URL that shows or changes per-visitor data is being speculated. Add it to the exclusion list from step 4.

When this is the wrong approach

Speculative loading only speeds up the second and later pages of a visit. It does nothing for the first page or for slow server response, so a slow site needs work on the server and cache first.

Turn it off when your Content Security Policy can’t allow the inline rule, or when the site depends on GET links that change data and you can’t list them. Put this in the must-use plugin:

add_filter( 'wp_speculation_rules_configuration', '__return_null', 100 );

Priority 100 runs after the Speculative Loading plugin’s own filter at the default priority of 10, so the plugin can’t turn it back on. Purge the page cache, then repeat step 2: no output means it is off. More background is in the WordPress 6.8 developer note.

Running WordPress that earns real money deserves an operator — Stack Harbor runs that stack under Enterprise WordPress; book an infrastructure consult.