Instant page loads with Turbolinks and prefetch
Use Turbolinks and prefetching to get near instant page loads in a server rendered web application.
Turbolinks can dramatically speed up your website by not requiring and evaluating CSS and Javascript for each pageload. Instantclick will preload any links when you hover over them instead of when you click on one. What about having Turbolinks and “instant clicks”?
Introduction
The technology has been around for a while and has had a rocky history to say the least. Some love it because of the often vastly improved page load times. Others hate it because of gotchas and having to track down weird event binding bugs persisting between page loads. There is no denying that it can make a big difference however as shown in the 100ms to Glass with Rails and Turbolinks post. But what if you can have essentially 0ms to Glass? That sounds better. Let’s do that.
How Turbolinks works
Turbolinks speeds up your site by intercepting any requests made from clicking on links for example. Instead of letting the browser do a full page load and changing the page it instead loads the content with AJAX and replaces the body of the page with the body of the new page. This means that the browser does not have to interpret any CSS or JS assets from the new page. The speed improvement from doing this can be huge if you have big assets like a lot of Javascript. Because of how Turbolinks works there are some issues and gotchas however. For example you cannot use the standard $(document).ready
for pages loaded by Turbolinks. You have to use the events coming from Turbolinks instead. There are also issues with event handlers binding twice if you are not careful. Nonetheless, Turbolinks can be a huge improvement for many applications.
Instantclick
Instantclick is similar to Turbolinks because it also loads the pages with AJAX and replaces the body on page change. The major difference is that Instantclick starts loading the new page when a use hovers over the link. When the user then clicks the link it does the body replacement. In most cases this means that the new page is already loaded when the user clicks the link and the replacement happens immediately. The site will feel like it changed the page in an instant with no delay whatsoever for network load and such.
Instantclick has a test page where you can test your speed skills. I tried it without “trying too hard” and this was the result:
As you can see, we have almost 200ms here to do something before the user expects something to happen on the screen. If a new page is loaded in this time and is ready in the chamber when the click goes through then the page will seem instant. Exciting stuff!
Combining ideas from Turbolinks and Instantclick
So now we loop back to Turbolinks. You can’t simply drop in Instantclick in a Turbolinks enabled application and call it a day. This won’t work. You can of course use only InstantClick but what if you want to use the other benefits from Turbolinks and still get the snappy performance?
Turns out that some clever people have thought of this already and opened up a discussion on Github. Further into the discussion is an implementation of this. That solution works fine but I wanted it to work without having to use jQuery since I don’t use it in my current project. Here is the version I made without the jQuery dependency.
You need to have hoverintent installed for this to work.
document.addEventListener("turbolinks:load", () => {
var hoverIntentOptions = {
interval: 50,
sensitivity: 5
};
document.querySelectorAll("a").forEach(node => {
if (node.dataset.turbolinks === "false") {
return;
}
var prefetcher;
hoverintent(
node,
function() {
var href = this.getAttribute("href");
if (!href.match(/^\//)) {
return;
}
if (prefetcher) {
if (prefetcher.getAttribute("href") != href) {
prefetcher.getAttribute("href", href);
}
} else {
var link = document.createElement("link");
link.setAttribute("rel", "prefetch");
link.setAttribute("href", href);
prefetcher = document.body.appendChild(link);
}
},
function() {}
).options(hoverIntentOptions);
});
});
Again, this is not my code originally and the credit for this should go to grosser and Enalmada for coming up with the original solution.
This simply hooks into the turbolinks:load
event and will run on every page change after Turbolinks has done its thing. It will loop all the anchor tags on the site, ignore all the ones not using Turbolinks and then add the desired behavior to all of them. The use of hoverintent is due to the fact that you probably don’t want to preload all pages when the user simply moves the cursor across the screen. Hoverintent will instead trigger events when the cursor movement has slowed down. You can fiddle with the sensitivity and the interval but I found that these settings worked for me.
The main part of the code is to insert a link at the end of the document whenever a user hovers over an enabled anchor tag. It takes advantage of a technique called Link prefetching. When a browser has finished loading a page it will automatically watch for links with rel='prefetch
and load them into the cache in the background. When these URLs are then requested they are simply fetched from the cache and loaded. Thus, the instant magical performance is achieved. The code will also make sure that it doesn’t insert duplicate links if the user hovers over the same tag again. It also makes sure that only local links will be prefetched.
For mobile you can use the touchStart event but since the 300ms delay has been removed in mobile Chrome and Safari then it makes little sense to do this there.