---
title: "It's hard to speculatively load what tomorrow may bring."
date: 2025-04-23T11:32:03Z
modified: 2025-04-23T11:34:33Z
permalink: "https://dgw.ltd/2025/04/23/its-hard-to-speculatively-load-what-tomorrow-may-bring/"
type: post
status: publish
excerpt: ""
wpid: 541
categories:
  - Code
featured_image: "https://dgw.ltd/wp-content/uploads/2025/04/rocket.png"
featured_image_alt: Rocket emoji
---

It appears the shiny new WordPress version 6.8 comes with Speculation Rules [out of the box](https://make.wordpress.org/core/2025/03/06/speculative-loading-in-6-8/). This is a really interesting optimisation feature, enabling us to programmatically tell the browser ([Chrome](https://caniuse.com/?search=Speculation%20Rules%20)) to get pages ready for the user to visit. It’s pretty dense and complex, but potentially a great optimisation option. Basically it behaves as if the page has been opened in an invisible background tab. As the [API states](https://developer.chrome.com/docs/web-platform/prerender-pages):

> This can therefore also have a direct impact on a site’s [Core Web Vitals](https://web.dev/articles/vitals), with near zero LCP, reduced CLS (since any load CLS happens before the initial view), and improved INP (since the load should be completed before the user interacts).

This is a slightly spooky feature that I’ve applied manually to this site via data attributes, mostly on the menu as this is where I believe we want most of the goodness for our spicy preloads. I’ve taken inspiration from the excellent [CSS Wizardry](https://csswizardry.com/2024/12/a-layered-approach-to-speculation-rules) who walks us through the [Speculation Rules API](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API) where we look at the following approaches:

**`prefetch`** pays the next page’s TTFB costs up-front and ahead of time;

**`prerender`** pays the next page’s TTFB, FCP, and LCP up-front.

The tl;dr for my implementation is we add data-prefetch=”prerender” to all top level menu items and data-prefetch (no value) to sub menus. This immediately _prerender_s top level items and immediately _prefetch_es secondary level items but _prerendered_ on demand. All other links are dormant until they get prefetched on demand.

For example:


```html
<a href data-prefetch>Prefetched Link</a>
<a href data-prefetch=prerender>Prerendered Link</a>
```

It’s great that WP are applying this but on initial viewing it appears the application is a little….thirsty, and potentially underpowered, however I am sure we’ll gain additional tools to extend and improve this on later releases. Currently it appears you can change the eagerness and add URLs paths to an exclusion list.

This is what it appears to be doing:

- One rule targeting all in‑site links (href\_matches: “/\*”), minus a big exclusion list
- Single eagerness: conservative (on click) – avoids overload but may under‑prefetch pages
- Every internal URL except wp‑admin, uploads, no‑prefetch classes, nofollow links

This appears a good base to start from, however in the CSS Wizardry post, many of the ‘eagerness’ rules are ‘immediate’, ‘moderate’, WP’s appears to be mostly set as ‘conservative’.

Here is the eagerness settings as outlined by a Chrome developers blog post about the [Speculation Rules API](https://developer.chrome.com/blog/speculation-rules-improvements):

**`immediate`:** This is used to speculate as soon as possible, that is, as soon as the speculation rules are observed.

**`eager`:** This currently behaves identically to the `immediate` setting, but in future, we are looking to place this somewhere between `immediate` and `moderate`.

**`moderate`:** This performs speculations if you hover over a link for 200 milliseconds (or on the `<a href="https://developer.mozilla.org/docs/Web/API/Element/pointerdown_event">pointerdown</a>` event if that is sooner, and on mobile where there is no `hover` event).

**`conservative`:** This speculates on pointer or touch down.

As such the WP approach appears to be a good one for a “set‑and‑forget” broad prefetch on a content site, with prefetch only. However I really liked the CSS Wizardry approach which allows us some control over what get’s prefetched **and** prerendered.

So that said, I’ve decided to disable the new core Speculation rules that comes out of the box on 6.8, via the following:


```php
add_filter(
    'wp_speculation_rules_configuration',
    function ( $config ) {
        // Returning null disables all speculative loading rules.
        return null;
    }
);
```

And have applied the data attributes to my menu via WP\_HTML\_Tag\_Processor and walker\_nav\_menu\_start\_el


```php
if ( ! function_exists( 'dgwltd_speculation_rules' ) ) :
    function dgwltd_speculation_rules( $item_output, $item, $depth, $args ) {

		if ( 'primary' === $args->theme_location ) {

			$tags = new WP_HTML_Tag_Processor( $item_output );

			// Add data-prefetch=prerender to top-level menu links
			
			// Top-level menu items are immediately prerendered
			// Sub-menu items are immediately prefetched but prerendered on demand (hover for 200ms)
			
			if ( $item->menu_item_parent == 0 ) {
				$tags->next_tag( 'a' );
				$tags->set_attribute( 'data-prefetch', 'prerender' );
			} else {
				$tags->next_tag( 'a' );
				$tags->set_attribute( 'data-prefetch', '' ); // Add the attribute without a value
			}

			return $tags->get_updated_html();
		}

		return $item_output;

    }
    add_filter( 'walker_nav_menu_start_el', 'dgwltd_speculation_rules', 10, 4 );
endif;
```

And the full speculation rules placed in the footer


```js
<script type="speculationrules">
  {
    "prefetch": [
      {
        "where": {
          "selector_matches": "[data-prefetch]"
        },
        "eagerness": "immediate"
      },
      {
        "where": {
          "and": [
            { "href_matches": "^/.*" },
            { "not": {
                "href_matches": [
                  "\/wp-*.php",
                  "\/wp-admin\/*",
                  "\/wp-content\/uploads\/*",
                  "\/wp-content\/*",
                  "\/wp-content\/plugins\/*",
                  "\/wp-content\/themes\/<?php echo sanitize_title($theme_slug); ?>\/*",
                  "\/*\\?(.+)"
                ]
              }
            },
            {
            "not": {
                "selector_matches": "a[rel~=\"nofollow\"]"
              }
            },
            {
            "not": {
                "selector_matches": ".no-prefetch, .no-prefetch a"
              }
            }
          ]
        },
        "eagerness": "moderate"
      }
    ],
    "prerender": [
      {
        "where": {
          "selector_matches": "[data-prefetch=prerender]"
        },
        "eagerness": "immediate"
      },
      {
        "where": {
          "selector_matches": "[data-prefetch]"
        },
        "eagerness": "moderate"
      }
    ]
  }
</script>
```