Web Performance Calendar

The speed geek's favorite time of year
2024 Edition
ABOUT THE AUTHOR
Photo of Quynh-Chi (QC) Nguyen

Quynh-Chi (QC) Nguyen (@qcn.bluesky.social) currently works as a staff software engineer on the platform team at an edtech company, delivering a SaaS textbook replacement platform. She's collected a range of professional interests over several years working in both product and infrastructure. Sometimes she speaks or writes about them, which results in writing about herself in the third person. She breaks things a lot. Occasionally she fixes them.

QC is a very normal human being and definitely not a rabbit who likes Vim, bunnies, and Having Opinions. Working is just a lazy way to fund her board game collection.

You can find her on Github at @qcn or Bluesky at @qcn.bluesky.social.

Speculative loading, or navigation speculation, is the concept of predicting (speculating about) which page a user might visit next and doing some or all of the work to load that page before they visit it. This way, if the user does end up navigating to the page, it appears to load much faster. Browsers have offered several types of resource hints to enable speculation for many years, but many of these interfaces have been clunky and inflexible to use. Google has recently introduced the Speculation Rules API in Chromium, designed to make it easier to prefetch and prerender documents for future navigations.

When you navigate to a URL, the flow for the browser looks something like:

  • do a DNS lookup on the domain name
  • execute the TCP handshake to open a connection to the server
  • execute the TLS handshake to agree on how to secure the connection (at least, I hope it’s doing this!)
  • request the document from the server
  • receive document from server
  • parse document, request and receive subresources, execute scripts, etc
  • paint final result to your screen

There are many stages in this workflow, and existing resource hints allow us to carry out several of them in advance. However, these often have limitations which should be understood to avoid wasted work. For example:

  • dns-prefetch does the DNS lookup in advance
  • preconnect does the DNS lookup, TCP handshake, and TLS handshake, so the connection is ready to go when a resource is requested; however, browsers close an unused connection after 10 seconds, so you shouldn’t preconnect unless you’re going to use the connection soon.
  • prefetch fetches resources so they’re already cached and don’t need to be downloaded on navigation. This relies on the browser’s HTTP cache, so it’s subject to being blocked by Cache-Control headers, and can’t be used for cross-site speculation due to cache partitioning.

These speculations also require the developer to explicitly identify the resource/URL that should be speculated on, and trigger the speculation itself. For example, this might be based on user behaviour: you could insert a new <link rel="prefetch"> tag to prefetch the target when a link is hovered over or clicked on. Another approach might be to use your site’s navigation metrics to determine likely navigation flows based on user activity, and speculate on the most likely next destinations.

Google’s Speculation Rules API, which is available in Chromium-based browsers since Chrome 109, offers a more ergonomic and full-featured approach to prefetch and prerender. There is currently no support in Firefox (see the tracking issue for their position, which they recently changed to “positive” for prefetch with “interested in implementing”) and Safari (tracking issue here). However, as it’s a progressive API, you can use it and browsers that don’t support it will simply ignore it. You can use feature-detection to fall back to supported forms of speculation if you wish.

Prerender is the big deal when it comes to Speculation Rules. Chrome itself now prerenders some pages when you use the browser’s address bar to navigate or search, and when you navigate using the bookmark bar. The Speculation Rules API allows developers to trigger the same kind of prerender to load their own pages.

While there’s a legacy prerender interface <link rel="prerender"> (which is, like Speculation Rules, only supported in Chromium) it doesn’t trigger a full prerender – the feature was originally implemented as a full prerender but as this was too resource-heavy and complex to support, it was replaced with NoState Prefetch. This type of prefetch will fetch the specified resource and a limited set of its subresources, populating the HTTP cache as for <link rel="prefetch">, but doesn’t render the page or execute any scripts.

Speculation Rules prerender, on the other hand, is a full prerender. When a Speculation Rules prerender is triggered, Chrome renders the target page in an invisible tab, using a per-document, in-memory cache. When the user subsequently navigates to that prerendered page, the browser makes that invisible tab active; the resulting user experience is that the page loaded (almost) instantly, because it was already rendered and everything was in memory. This is beneficial even if the user navigates before the page finishes prerendering: the browser still activates the invisible tab immediately and continues the rendering process, and since the page was already partially loaded, the user doesn’t have to wait as long for it to load from scratch.

Because this method actually renders the page and executes most JavaScript, it’s also able to load subresources that are loaded via JavaScript, unlike <link rel="prerender">.

Speculation Rules prefetch uses the same per-document in-memory cache as prerender to store prefetched documents. Since it doesn’t rely on the HTTP cache, it doesn’t get blocked by Cache-Control headers, but it can still populate the HTTP cache given suitable response headers. However, as it’s a document-based prefetch and doesn’t parse the document, it can’t prefetch subresources – if you want to do that, you’ll still need to use one of the existing approaches where you can specify the subresources explicitly.

Another tradeoff of the Speculation Rules API being designed for navigation between documents is that it works only for multi-page apps. Single page apps don’t do real page navigations that the browser manages, even if they’re using a router that makes it look like the page URL is changing – they only fetch new data to render within the current document. So you could prerender the single page app itself from a previous page, and that could help with the initial load time of the app, but you can’t use Speculation Rules for navigations within it.

As previously mentioned, legacy approaches to speculation are fairly inflexible in how resources to be speculated on are specified – the developer needs to specify the exact resource to be fetched, and determine when to trigger speculation. The Speculation Rules syntax is more flexible, and it allows the browser to do a lot of the work in deciding what to speculate on and when.

Under the Speculation Rules API, you use JSON to define rules that tell the browser what to speculatively load. You can still specify exact URLs, but you can also define rules about how to match links in the current page: the browser will scan over all the anchor and area tags in the document to look for links, and find the ones that match the rules. You can pattern-match URLs and match by CSS selector, and you can combine and negate conditions. This is much nicer than having to create <link> elements for each URL explicitly.

Additionally, you can set an eagerness level for each rule, which hints how important this speculation is and lets the browser take care of it. In order of importance, these levels are:

  • immediate – the browser should trigger this speculation as soon as possible
  • eager – at present, Chrome treats this the same way as immediate
  • moderate – currently triggered after hovering the link for 200ms
  • conservative – to minimise resource wastage, the browser should only start the speculation when the user activates the link (touch or pointerdown—a few milliseconds ahead of the actual click being registered)

While Chrome currently implements these eagerness levels as described above, future browser developers could choose their own heuristics for determining an increased likelihood of navigating to the link. You should consider the characteristics of your site when you’re choosing how eager the speculation should be – if your pages are just static HTML, speculating on that is going to create a lot less overhead than if you have really heavy pages with a lot of Javascript, where you might want to be more conservative because the impact of that potential wasted work is higher.

Note that Chrome will also consider resource constraints and user settings in determining how to prioritise speculation. In addition to users being able to disable speculation completely, there’s a list of reasons why Chrome might disable or deprioritise speculation, including (but certainly not limited to) the computer being in battery saver mode or low on memory. Note that not all the reason codes are implemented in Chrome  (low bandwidth for example is not implemented) and some (such as service workers) apply only to prefetch, and others (cookies) apply only to cross-origin prefetches.

There are also limits to how many speculations will be done at a time. For example, the browser will queue at most two moderate or conservative speculations. If a third non-eager speculation is triggered, the first one will be cancelled, because the user is more likely to follow the link they’re currently interacting with.

Rules can be inserted directly into the page using a <script type="speculationrules"> element, eg:

<script type="speculationrules">
    {  
        "prerender": [  
            {  
                "where": { "selector_matches": ".prerender-me" },
                "eagerness": "eager"
            },  
            {  
                "where": { "href_matches": "/*bunnypics*" },
                "eagerness": "moderate"  
            }  
        ],  
        "prefetch": [
            {
                "where": { "selector_matches": ".prefetch-me" },
                "eagerness": "immediate"
            }  
        ]  
    }  
</script>

or defined in one or more files and specified using a Speculation-Rules response header, which allows use of the API without having to change the document itself:

Speculation-Rules: "/rules/prefetch.json","/rules/prerender.json"

As the Speculation Rules API is specifically designed for navigation, it can sometimes be used to speculate on cross-site or cross-origin navigation. Specifically:

  • The site is a boundary for privacy. Same-site prefetches, whether same- or cross-origin, are allowed, because prefetch doesn’t execute scripts and the speculation doesn’t cross the privacy boundary. However, cross-site prefetches only work if the browser doesn’t already have cookies set for the destination site. This prevents sites from tracking user activity through prefetches, using existing cookies. In future, Chromium may allow cross-site prefetches via opt-in from the destination server.
  • The origin is a boundary for security. Prerender executes scripts which may be a security risk, so while same-origin prerender is permitted, cross-origin/same-site prerender is only allowed if the destination server opts in using the Supports-Loading-Mode response header. Cross-site prerender is not allowed.

However, even with the built-in limitations, it’s important to note that there are still privacy considerations when using speculation! Prefetching a page intrinsically provides the destination server with information about who the user is and what page they’re currently browsing – in fact, the act of prefetching in itself could reveal information about their setup. For example, since speculation is blocked in energy saver mode, the act of speculating implies the user isn’t using that mode. Similarly, if the speculation rules are based on analyzing user activity and predicting where people with similar usage patterns go next, making a prefetch request could leak information about what kind of pages the user has visited. It’s important to consider the privacy tradeoffs when choosing to use speculation – the API alone won’t protect you from everything. For reasons like these, speculation is often disabled in the browser by privacy-focused extensions like uBlock Origin.

You should also keep in mind that even though prefetching and prerendering can make navigation faster for your users, it’s not a substitute for optimising your download sizes. That data is going to be transferred whether you do it before or after navigation, so it’s still subject to all the normal constraints, like slow internet connections or data transfer limits – and speaking as a platform engineer, you’re still paying for the bandwidth!

This makes speculative loading a bit of a gamble. If it works out well, it can have great benefits and your users will love you for making their experience so fast! But if you load the wrong things, it’ll cost you and your users, in wasted data transfer, resource usage, and potentially extreme confusion.

The obvious case is if the user doesn’t end up visiting that page next. Everything else you prefetch or prerender gets discarded from the in-memory cache if you navigate to a different page, so if you prerender two possible next destinations from the current page, at least one of those is going to be thrown out on navigation, maybe both. It doesn’t matter if there would have been a subsequent navigation to the other page – the cached prerender is already gone! If your prerender stored anything in the HTTP cache, that will still be there, but even with this small benefit you still probably want to avoid a wasted prerender because it’s doing so much background work. You might decide you’re okay with this scenario for a prefetch, because it’s much lighter on resources.

However, the page might also change before it’s loaded! Consider the case of prerendering a thread on a discussion forum, where the comments depend on server state. If another user posts a new comment after your browser prerenders the thread and then you navigate to the prerendered page, the comments are out of date – that isn’t so bad. But if you’re a moderator on the forum and you just deleted a comment, then you navigate to the prerendered thread and it looks like it’s still there, that’s confusing.

That also applies for state that’s changed by the user. Say you visit a site that defaults to dark mode, and you switch to light mode. Your preference is saved locally in your browser, without reloading the page. Then you navigate to the next page, but it was prerendered before you changed to light mode, so it shows up in dark mode – that’s confusing too!

In these situations, it’s best for pages to know how to refresh themselves if the state might change. However, you won’t always have control over the pages you’re prerendering or the server they come from, so you need to be aware of these possibilities.

It gets worse, though – these examples demonstrated the user seeing an inconsistent state, which was inconvenient but mostly confusing to the user. But another pattern where it’s not safe to prerender or prefetch is where making a request for a page has side effects – it initiates some action or has some effect that you don’t want to trigger until you actually navigate there. For example, requesting the page could log the user out, change their preferred language, or use up free articles on a paywalled site. If you have control over the server those pages are coming from, you can detect that the request is because of a prefetch or prerender and defer those actions until the navigation actually happens.

Similarly, there are a number of web and browser APIs and features that are deferred or restricted when prerendering by default, because they trigger behaviours that shouldn’t activate until the page is activated. For example, you don’t want video or audio to start playing for a page you haven’t navigated to yet!

Fortunately for developers, Chrome has implemented new developer tools that help to see how speculation is working, but are particularly handy to figure out why it’s not working. They provide a useful guide for debugging speculation rules in their developer documentation.

Further reading:

Leave a Reply

You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>
And here's a tool to convert HTML entities