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

推荐订阅源

L
Lohrmann on Cybersecurity
Martin Fowler
Martin Fowler
Engineering at Meta
Engineering at Meta
腾讯CDC
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Microsoft Azure Blog
Microsoft Azure Blog
G
Google Developers Blog
TaoSecurity Blog
TaoSecurity Blog
博客园_首页
Vercel News
Vercel News
Hugging Face - Blog
Hugging Face - Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Last Week in AI
Last Week in AI
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
The Exploit Database - CXSecurity.com
量子位
Project Zero
Project Zero
A
Arctic Wolf
小众软件
小众软件
NISL@THU
NISL@THU
C
CERT Recently Published Vulnerability Notes
有赞技术团队
有赞技术团队
MongoDB | Blog
MongoDB | Blog
博客园 - 聂微东
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
N
News and Events Feed by Topic
宝玉的分享
宝玉的分享
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Troy Hunt's Blog
P
Privacy & Cybersecurity Law Blog
Security Latest
Security Latest
B
Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
D
DataBreaches.Net
Schneier on Security
Schneier on Security
The Hacker News
The Hacker News
K
Kaspersky official blog
C
Check Point Blog
Hacker News: Ask HN
Hacker News: Ask HN
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Webroot Blog
Webroot Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
人人都是产品经理
人人都是产品经理
AI
AI
Cisco Talos Blog
Cisco Talos Blog
MyScale Blog
MyScale Blog
Cloudbric
Cloudbric
B
Blog RSS Feed
S
Schneier on Security
P
Palo Alto Networks 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()? Speeding Up Async Snippets Critical CSS? Not So Fast! Measure What You Impact, Not What You Influence Optimising Largest Contentful Paint Measuring Web Performance in Mobile Safari Site-Speed Topography Speed Up Google Fonts Real-World Effectiveness of Brotli Performance Budgets, Pragmatically Lazy Pre-Browsing with Prefetch Making Cloud.typography Fast(er) Time to First Byte: What It Is and How to Improve It Self-Host Your Static Assets Tips for Technical Interviews Cache-Control for Civilians Bandwidth or Latency: When to Optimise for Which ITCSS × Skillshare What If? CSS and Network Performance The Three Types of Performance Testing Getting to Know a Legacy Codebase Image Inconsistencies: How and When Browsers Download Images Identifying, Auditing, and Discussing Third Parties My Digital Music Setup Measuring the Hard-to-Measure Finding Dead CSS The Fallacies of Distributed Computing (Applied to Front-End Performance) Ten Years Old Relative Requirements Airplanes and Ashtrays Performance and Resilience: Stress-Testing Third Parties Refactoring Tunnels Little Things I Like to Do with Git Writing Tidy Code Configuring Git and Vim Base64 Encoding & Performance, Part 2: Gathering Data Base64 Encoding & Performance, Part 1: What’s Up with Base64? Code Smells in CSS Revisited Typography for Developers Moving CSS Wizardry onto HTTPS and HTTP/2 Ack for CSS Developers A New Year, a New Focus Preparing Vim for Apple’s Touch Bar Choosing the Correct Average CSS Shorthand Syntax Considered an Anti-Pattern CSS Wizardry Newsletter Nesting Your BEM? Improving Perceived Performance with Multiple Background Images Continue Normalising Your CSS Pure CSS Content Filter Pragmatic, Practical, and Progressive Theming with Custom Properties Refactoring CSS: The Three I’s Speaker’s Checklist: Before and After Your Talk Improving Your CSS with Parker The Importance of !important: Forcing Immutability in CSS Mixins Better for Performance Managing Typography on Large Apps White October Events Workshop Partnership BEMIT: Taking the BEM Naming Convention a Step Further Travelling Like You Want to, When You Have To Contextual Styling: UI Components, Nesting, and Implementation Detail Subtleties with Self-Chained Classes Cyclomatic Complexity: Logic in CSS Immutable CSS Can CSS Be Too Modular? More Transparent UI Code with Namespaces When to use @extend; when to use a mixin The Specificity Graph CSS Wizardry Ltd.: Year 1 in review CSS Guidelines 2.0.0
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.