惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

A
About on SuperTechFans
Y
Y Combinator Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Microsoft Security Blog
Microsoft Security Blog
aimingoo的专栏
aimingoo的专栏
I
InfoQ
C
Check Point Blog
IT之家
IT之家
MyScale Blog
MyScale Blog
Apple Machine Learning Research
Apple Machine Learning Research
Vercel News
Vercel News
Last Week in AI
Last Week in AI
GbyAI
GbyAI
P
Proofpoint News Feed
量子位
Stack Overflow Blog
Stack Overflow Blog
Microsoft Azure Blog
Microsoft Azure Blog
月光博客
月光博客
阮一峰的网络日志
阮一峰的网络日志
人人都是产品经理
人人都是产品经理
B
Blog
T
The Blog of Author Tim Ferriss
H
Help Net Security
云风的 BLOG
云风的 BLOG

CSS Wizardry

Front-End’s Missing Metric: The TBT Window Meet Your Users Where They Are with Obs.js Better Browser Caching with No-Vary-Search font-family Doesn’t Fall Back the Way You Think What Is CSS Containment and How Can I Use It? When All You Can Do Is All or Nothing, Do Nothing Obs.js: Context-Aware Web Performance for Everyone Low- and Mid-Tier Mobile for the Real World (2025) The Fastest Site in the Tour de France Making Sense of the Performance Extensibility API Why Do We Have a Cache-Control Request Header? HTML Is Not a Programming Language… Build for the Web, Build on the Web, Build with the Web Licensing Code on CSS Wizardry Designing (and Evolving) a New Web Performance Score Core Web Vitals Colours The Ultimate Contract Templates for Tech Consultants: Protect Your Business and Get Paid Optimising for High Latency Environments Cache Grab: How Much Are You Leaving on the Table? blocking=render: Why would you do that?! Correctly Configure (Pre) Connections The Three Cs: 🤝 Concatenate, 🗜️ Compress, 🗳️ Cache What Is the Maximum max-age? How to Clear Cache and Cookies on a Customer’s Device The Ultimate Low-Quality Image Placeholder Technique Core Web Vitals for Search Engine Optimisation: What Do We Need to Know? The HTTP/1-liness of HTTP/2 In Defence of DOM­Content­Loaded Site-Speed Topography Remapped Why Not document.write()?
A Layered Approach to Speculation Rules
Harry Roberts · 2024-12-02 · via CSS Wizardry

(last updated on )

Written by on CSS Wizardry.

Table of Contents

Independent writing is brought to you via my wonderful Supporters.

  1. Speculation Rules
  2. Speculation Rules on csswizardry.com
  3. A Multi-Tiered Approach
    1. Opt-In Strategy
    2. Opt-Out Strategy
    3. Layering Up
  4. Clearing Speculation Rules’ Cache With Clear-Site-Data

I’ve always loved doing slightly unconventional and crafty things with simple web platform features to get every last drop out of them. From building the smallest compliant LCP, lazily prefetching CSS, or using pixel GIFs to track non-JS users and dead CSS, I find a lot of fun in making useful things out of other useful things.

Recently, I’ve been playing similar games with the Speculation Rules API.

Need Some Help?

I help companies find and fix site-speed issues. Performance audits, training, consultancy, and more.

Speculation Rules

I don’t want to go super in-depth about the Speculation Rules API in this post, but the key thing to know is that it provides two speculative loading types—prefetch and prerender—which ultimately have the following goals:

  • 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.

It’s going to be very helpful to keep those two truisms in mind—prefetch for paying down TTFB; prerender for LCP. This makes prefetch the lighter of the two and prerender the more resource-intensive.

That’s about all you need to know for the purposes of this article.

Speculation Rules on csswizardry.com

Ever since Speculation Rules became available, I’ve used them in somewhat uninspired ways on this site:

  • to prerender the latest article from the homepage:
    <script type=speculationrules>
      {
        "prerender": [
          {
            "urls": [ "/2024/12/a-layered-approach-to-speculation-rules/" ]
          }
        ]
      }
    </script>
    
  • to prerender the next and previous articles from a page such as this one:
    <script type=speculationrules>
      {
        "prerender": [
          {
            "urls": [
                "/2024/12/a-layered-approach-to-speculation-rules/",
                "/2024/11/core-web-vitals-colours/"
            ]
          }
        ]
      }
    </script>
    

In this scenario, I am explicitly prerendering named and known URLs, with a loose idea of a potential and likely user journey—I’m warming up what I think might be the visitor’s next page.

While these are both functional and beneficial, I wanted to do more. My site, although not very obviously, has two sides to it: the blog, for folk like you, and the commercial aspect, for potential clients. While steering people down a fast article-reading path is great, can I do more for visitors looking around other parts of the site?

With this in mind, I recently expanded my Speculation Rules to:

  1. immediately prefetch any internal links on the page, and;
  2. moderately prerender any other internal links on hover.

This fairly indiscriminate approach casts a much wider net than listed URLs, and instead looks out for any internal links on the page:

<script type=speculationrules>
  {
    "prefetch": [
      {
        "where": {
          "href_matches": "/*"
        },
        "eagerness": "immediate"
      }
    ],
    "prerender": [
      {
        "where": {
          "href_matches": "/*"
        },
        "eagerness": "moderate"
      }
    ]
  }
</script>

This slightly layered approach allows us to immediately pay the TTFB cost for all internal links on the page, and pay the LCP cost for any internal link that we hover (moderate). These are quite broad rules as they apply to any href on the page that matches /*—so any root-relative link at all.

This approach works well for me as my site is entirely statically generated and served from Cloudflare’s edge. I also don’t get masses of traffic, so the risk of increased server load anywhere is minimal. For sites with lots of traffic and highly dynamic back-ends (database queries, API calls, insufficient caching), this approach might be a little too liberal.

Need Some Help?

I help companies find and fix site-speed issues. Performance audits, training, consultancy, and more.

A Multi-Tiered Approach

On a recent client project, I wanted to take the idea further. They have a large and relatively complex site (many different product lines sitting under one domain) with lots of traffic and a nontrivial back-end infrastructure. Things would have to be a little more considered.

Opt-In Strategy

They’re a Big Site™ so an opt-in approach was the better way to go. A wildcard-like match would prove far too greedy1, and as different pages contain vastly different amounts of links, the additional overhead was difficult to predict on a site-wide scale.

Arguably the easiest way to opt into Speculations is with a selector. For example, we could use classes:

<a href class=prefetch>Prefetched Link</a>
<a href class=prerender>Prerendered Link</a>

And the corresponding Speculation Rules:

<script type=speculationrules>
  {
    "prefetch": [
      {
        "where": {
          "selector_matches": ".prefetch"
        },
        ...
      }
    ],
    "prerender": [
      {
        "where": {
          "selector_matches": ".prerender"
        },
        ...
      }
    ]
  }
</script>

N.B. As prerender already includes the prefetch phase, you’d never need both class="prefetch prerender"; one or the other is sufficient.

However, I’m very fond of this pattern:

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

And their respective Speculation Rules:

<script type=speculationrules>
  {
    "prefetch": [
      {
        "where": {
          "selector_matches": "[data-prefetch='']"
        },
        ...
      }
    ],
    "prerender": [
      {
        "where": {
          "selector_matches": "[data-prefetch=prerender]"
        },
        ...
      }
    ]
  }
</script>

It keeps all logic nicely and neatly contained in a data-prefetch attribute.

Note that I’m using [data-prefetch='']. This matches data-prefetch exaxtly. If I were to use [data-prefetch], it would match any and all of the following:

  • <a href data-prefetch>
  • <a href data-prefetch=prerender>
  • <a href data-prefetch=foo>
  • <a href data-prefetch='baz bar foo'>
  • <a href data-prefetch=false>

The last one is the one I care about the most, and will become very important right about… now.

Opt-Out Strategy

We’ll probably run into a scenario at some point where we explicitly want to opt out of prefetching or prerendering—for example, a log-out page. In order to be able to achieve that, we’ll need to reserve something like data-prefetch=false.

If we’d used "selector_matches": "[data-prefetch]" above, that would also match data-prefetch=false, which is exactly what we don’t want. That’s why we bound our selector onto "selector_matches": "[data-prefetch='']" specifically—only match a data-prefetch attribute that has no value.

Now, we have the following three explicit opt-in and -out hooks:

  • data-prefetch: Only prefetch this link.
  • data-prefetch=prerender: Make a full prerender for this link.
  • data-prefetch=false: Do nothing with this link.
<a href data-prefetch>Prefetched Link</a>
<a href data-prefetch=prerender>Prerendered Link</a>
<a href data-prefetch=false>Untouched Link</a>

Anything else would fail to match any Speculation Rule, and thus would do nothing.

Layering Up

With these simple opt-in and -out mechanisms in place, I wanted to look at ways to subtly and effectively layer this up to add further disclosed functionality without any additional configuration. What could I do to really maximise the benefit of Speculation Rules with just these two attributes?

My thinking was that if we’re explicitly marking data-prefetch and data-prefetch=prerender, could we upgrade the former to the later on-demand? When the page loads, the browser immediately fulfils its prefetches and prerenders, but when someone hovers a prefetched link, expand it to a full prerender?

Easy.

And then, for good measure, can we upgrade any other internal link from nothing to prefetch on demand?

Also easy!

Working from most- to least-aggressive, and keeping in mind our two truisms, the best way to think about what we’re achieving is that we:

  1. immediately pay LCP costs for any matching link we’ve opted into:
    "prerender": [
      {
        "where": {
          "selector_matches": "[data-prefetch=prerender]"
        },
        "eagerness": "immediate"
      },
      ...
    ]
    
  2. immediately pay TTFB costs for any matching link we’ve opted into:
    "prefetch": [
      {
        "where": {
          "selector_matches": "[data-prefetch='']"
        },
        "eagerness": "immediate"
      },
      ...
    ],
    
  3. on demand, pay LCP costs for any link we’ve already paid TTFB costs for:
    "prerender": [
      ...
      {
        "where": {
          "selector_matches": "[data-prefetch='']"
        },
        "eagerness": "moderate"
      }
    ]
    
  4. on demand, pay TTFB costs for any other internal links:
    "prefetch": [
      ...
      {
        "where": {
          "and": [
            { "href_matches": "/*" },
            { "not": { "selector_matches": "[data-prefetch=false]" } }
          ]
        },
        "eagerness": "moderate"
      }
    ],
    

    Note that here is where we prefetch any internal link except those explicitly opted out.

Now, the client has the ability to prerender highly likely or encouraged navigations with the data-prefetch=prerender attributes (e.g. on their top-level navigation or their homepage calls-to-action).

Things that are less likely but still reasonable candidates for warm-up (e.g. items in the sub-navigation) can simply carry data-prefetch.

All other internal links ("href_matches": "/*")—except the already-maxed out data-prefetch=prerender or opted-out data-prefetch=false—get upgraded to the next category on demand.

Putting them all together in the format and order required, our Speculation Rules look like this:

<!--! Content by Harry Roberts, csswizardry.com, available under the MIT license. -->

<script type=speculationrules>
  {
    "prefetch": [
      {
        "where": {
          "selector_matches": "[data-prefetch='']"
        },
        "eagerness": "immediate"
      },
      {
        "where": {
          "and": [
            { "href_matches": "/*" },
            { "not": { "selector_matches": "[data-prefetch=false]" } }
          ]
        },
        "eagerness": "moderate"
      }
    ],
    "prerender": [
      {
        "where": {
          "selector_matches": "[data-prefetch=prerender]"
        },
        "eagerness": "immediate"
      },
      {
        "where": {
          "selector_matches": "[data-prefetch='']"
        },
        "eagerness": "moderate"
      }
    ]
  }
</script>

We could apply these against this example page:

<ul class=c-nav>

  <li class=c-nav__main>
    <a href=/ data-prefetch=prerender>Home</a>

  <li class=c-nav__main>

    <a href=/about/ data-prefetch=prerender>About</a>

    <ul class=c-nav__sub>
      <li>
        <a href=/about/history/ data-prefetch>Company History</a>
      <li>
        <a href=/about/board/ data-prefetch>Company Directors</a>
    </ul>

  <li class=c-nav__main>

    <a href=/services/ data-prefetch=prerender>Services</a>

    <ul class=c-nav__sub>
      <li>
        <a href=/services/solutions/ data-prefetch>Solutions</a>
      <li>
        <a href=/services/industries/ data-prefetch>Industries</a>
    </ul>


  <li class=c-nav__main>
    <a href=/contact/ data-prefetch=prerender>Contact Us</a>

  <li class=c-nav__main>
    <a href=/log-out/ data-prefetch=false>Log Out</a>

</ul>

...

<a href=/sale/
   class=c-button
   data-prefetch=prerender>Black Friday Savings!</a>

...

<footer>
  <a href=/sitemap/>Sitemap</a>
</footer>
  • Top-level navigation items with data-prefetch=prerender (e.g. the About page) are immediately prerendered.
  • Sub-level navigation items with data-prefetch (e.g. the Solutions page) are immediately prefetched but prerendered on demand.
  • All other links (e.g. the Sitemap page) are dormant until they get prefetched on demand.
  • Any links with data-prefetch=false are skipped entirely.

I can’t publish any names or numbers or facts or figures, but we ran an experiment for a week and the outcomes we’re incredibly compelling.

I guess my point after all of this is that I think this is quite an elegant pattern and I’m quite happy with myself. If you’d like to be happy with me, too, I’m taking on new clients for 2025.

Clearing Speculation Rules’ Cache With Clear-Site-Data

In the upcoming Chrome 138 release, the Clear-Site-Data HTTP response header is being extended to add support for both the prefetchCache and prerenderCache directives. These can be used by developers to forcibly purge end-users caches in the event you may have incorrectly or misconfigured something in or around your Speculation Rules.

Thanks to Barry Pollard for sense-checks and streamlining.


Frequently Asked Questions

What are Speculation Rules?

Speculation Rules are browser instructions that let you prefetch or prerender likely future navigations so the next page can load much faster.

What is the difference between prefetch and prerender?

Prefetch fetches resources ahead of time, while prerender goes much further and prepares the destination page in advance so navigation can feel almost instant.

Why use a layered approach to Speculation Rules?

A layered approach lets you start with safer, cheaper behaviour and progressively add more aggressive speculation where the payoff is higher and the risks are acceptable.

Should every link be prerendered?

No. Prerendering everything is usually too expensive and too risky, so you should choose targets carefully and apply clear opt-in or opt-out rules.

How can I clear Speculation Rules cache?

One option is Clear-Site-Data, which can be used to explicitly wipe prefetched or prerendered state when you need a hard reset.