Building a prefetch custom element
Prefetching a resource that you might need is a well supported part of the HTML spec, but adding it can be based purely on assumption. How can we make it more dynamic and look for real hints?
What is prefetching
Resource Hints in HTML are a set of strategies for improving loading of pages and assets. Prefetch is one such strategy and can start the downloading content that is likely to be needed at some time in the future in the background.
Prefetching is an HTML feature, and requires no JavaScript. Until recently, this website used prefetching on the home page for the /writing
page as it was the main source of any content. It was as simple as adding an element to the document head:
<link rel="prefetch" href="/writing" />
The browser would prefetch the writing page when it was done with any tasks that have a higher priority; loading and rendering the current page and its resources. When visiting the writing page, it would be retrieved from the cache.
This worked fine, but is not at all dynamic. We have to make an assumption that a page is likely to be visited and this assumption gets weaker the more pages and links to pages a website has.
Making prefetching smarter
It'll become clear pretty quickly that this approach is only workable on a device with a pointing device, like a mouse or trackpad. The focus on using a hover state to detect intent to visit a page is completely unworkable on a touch device.
There is a delay between a link being hovered and being clicked. Hovering is a good hint that there is a click coming, either right away or at some point later, and loading the linked page in the background while resources are free will speed up the page transition when it's needed.
Click the button below a few times to see the delay between the mouseover
event and the following click
event, and an average of all attempts. Try and click naturally.
My own testing has showed that it's hard to get a hover time under 200ms even when trying to be fast, and for more normal point-and-click it's closer to 500ms.
Building the custom element
We don't need to use React or any other library to make prefetching dynamic. We can encapsulate everything needed into a custom element.
It's really easy to over-engineer this kind of thing. This implementation is very stripped back and there is definitely room for improvement, which we'll get into later.
If you're not familiar with custom elements, I have written about them before in Building a share button custom element which includes a reduced example of the simplest possible element. It might be helpful to read that first.
Approach
Before we dive into code, let's write out an approach:
- Create a custom element to wrap any links we want to prefetch
- Select the anchor element inside the custom element
- Select the
head
element of the document - Create a new
link
element, and set itsrel
andhref
attributes accordingly - On
mouseover
on the selected anchor element, append the createdlink
to thehead
We don't need to do anything to initiate the prefetch; the browser will handle that when the element is appended just as it would for a hardcoded link element.
Custom element source
There isn't anything too complex in this custom element. Let's look at the whole source and then break it down:
if ('customElements' in window) {
customElements.define(
'link-prefetch',
class extends HTMLElement {
connectedCallback() {
const aElem = this.querySelector('a');
const headElem = document.querySelector('head');
const linkElem = document.createElement('link');
linkElem.rel = 'prefetch';
linkElem.href = aElem.href;
aElem.addEventListener(
'mouseover',
() => {
headElem.appendChild(linkElem);
},
{ once: true }
);
}
}
);
}
Custom elements are class based, so in this case we extend the built in HTMLElement
. The connectedCallback
method is called once connected, and with this being a small and synchronous operation it can all stay within this method.
The approach above does a good job of describing the code inside the custom element. One thing of note is the relatively new options object that is passed as a third argument to addEventListener
. Using once ensures that the event is fired only once. Without this we would needlessly keep appending elements to the head on every hover.
Compared to JSX the native DOM API can feel a little clunky, especially when creating an element to append. It's a worthy tradeoff as this entire implementation is less than 1kb. React 16 and React DOM is more than 30kb before any custom code.
Once defined, the custom element should wrap any link whose destination should be prefetched:
<link-prefetch>
<a href="/writing" />
</link-prefetch>
Browser support
Support for both prefetch and custom elements is good. Safari is behind, though prefetch being behind a flag suggests it'll have stable support soon.
I don't think there is any benefit to trying to sniff for support for prefetching before appending the link element. It's certainly a more expensive operation than appending an element that current Safari versions can just ignore.
As with all custom elements, we check for customElements
in the window
object before we try and register. If the element isn't registered, the browser will just ignore the unknown <link-prefetch>
tag and carry on as normal. HTML is very good at ignoring nonsense: we can all learn from that.
Going further
This idea is definitely not mature and there are a few known problems:
- There is no error handling
- The fairly limited time for prefetching means this approach favours sites and connections that are already fast
- Setting wider bounds on the hover area would increase the window of time for prefetching, but would also increase complexity
- If there are multiple links to the same page, hovering each would append a duplicate
link
element - Touch devices don't really have the concept of hovering, so any kind of hint is near impossible to get
The bottom line, as always, is to build sites that are naturally fast and light in the first place.
This attempt at a speed boost is most effective where it is needed the least. I don't that negates the entire idea, but it certainly needs work to overcome it's pitfalls.
More posts
PreviousBuilding a share button custom element
NextDark mode support for your favicon