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 its rel and href attributes accordingly
  • On mouseover on the selected anchor element, append the created link to the head

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

Previous

Building a share button custom element

Next

Dark mode support for your favicon