Written by Harry Roberts on CSS Wizardry.
Table of Contents
- Speculation Rules
- Speculation Rules on
csswizardry.com
- A Multi-Tiered Approach
- Opt-In Strategy
- Opt-Out Strategy
- Layering Up
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.
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:speculationrules> { "prerender": [ { "urls": [ "/2024/12/a-layered-approach-to-speculation-rules/" ] } ] }
- to prerender the next and previous
articles
from a page such as this one:speculationrules> { "prerender": [ { "urls": [ "/2024/12/a-layered-approach-to-speculation-rules/", "/2024/11/core-web-vitals-colours/" ] } ] }
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:
immediate
lyprefetch
any internal links on the page, and;moderate
lyprerender
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:
speculationrules>
{
"prefetch": [
{
"where": {
"href_matches": "/*"
},
"eagerness": "immediate"
}
],
"prerender": [
{
"where": {
"href_matches": "/*"
},
"eagerness": "moderate"
}
]
}
This slightly layered approach allows us to immediate
ly 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.
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 greedy, 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:
And the corresponding Speculation Rules:
speculationrules>
{
"prefetch": [
{
"where": {
"selector_matches": ".prefetch"
},
...
}
],
"prerender": [
{
"where": {
"selector_matches": ".prerender"
},
...
}
]
}
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:
And their respective Speculation Rules:
speculationrules>
{
"prefetch": [
{
"where": {
"selector_matches": "[data-prefetch=""]"
},
...
}
],
"prerender": [
{
"where": {
"selector_matches": "[data-prefetch=prerender]"
},
...
}
]
}
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:
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.
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:
- immediately pay LCP costs for any matching link we’ve opted into:
"prerender": [ { "where": { "selector_matches": "[data-prefetch=prerender]" }, "eagerness": "immediate" }, ... ]
- immediately pay TTFB costs for any matching link we’ve opted into:
"prefetch": [ { "where": { "selector_matches": "[data-prefetch=""]" }, "eagerness": "immediate" }, ... ],
- on demand, pay LCP costs for any link we’ve already paid TTFB costs for:
"prerender": [ ... { "where": { "selector_matches": "[data-prefetch=""]" },
Leave a Reply