<link rel="prefetch">

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Looking at the code of this prefetch script, there is another way this can be used. If you add a data-prefetch-with-link="true" attribute to a link, instead of making an Ajax call, it will add a <link rel="prefetch"> element to the head tag of the page.

... lines 1 - 133
function preload(link) {
const url = link.getAttribute("href")
const loc = new URL(url, location.protocol + "//" + location.host)
const absoluteUrl = loc.toString()
if (link.dataset.prefetchWithLink == "true") {
const prefetcher = document.createElement('link')
prefetcher.rel = 'prefetch'
prefetcher.href = url
document.head.appendChild(prefetcher)
pendingPrefetches.delete(absoluteUrl)
} else if (!Turbo.navigator.view.snapshotCache.has(loc)) {
fetchPage(url, responseText => {
const snapshot = Snapshot.fromHTMLString(responseText)
Turbo.navigator.view.snapshotCache.put(loc, snapshot)
pendingPrefetches.delete(absoluteUrl)
})
}
}

What does that do? Great question! To explain, let's back up a little. So far, this whole prefetch script has been pure Turbo magic: it makes an Ajax call and stores it into Turbo's snapshot cache. But actually, your browser has a "prefetch" feature built into it! And that is what this data-prefetch-with-link code is leveraging.

To see how it works, close the prefetch script and comment out its import in app.js. I want to see how true prefetching works without any Turbo magic... because prefetching can be used on any site - even if it doesn't use Turbo.

16 lines assets/app.js
... lines 1 - 10
// start the Stimulus application
import './bootstrap';
import './turbo/turbo-helper';
//import './turbo/prefetch';

Here's the deal: imagine that, when a user goes to a specific page on our site, we're fairly sure that you know what the next page - or pages - will be that the user will go to. In that case, we can hint to the user's browser that, if it has some extra time, it can prefetch that URL so that if the user does navigate to it, it will load instantly from cache.

Let's try this. Add an item to your cart and then head to the cart page. It might be obvious that, once a user visits this page, they often click the "Check out" link next. So let's add a hint that the browser should "prefetch" that page.

How? Open the template for this page: templates/cart/cart.html.twig. On top, override a block called metas. This is not a standard Symfony block. But earlier in the tutorial, in base.html.twig, we added this.

Inside the block, add link - but instead of rel="stylesheet", use rel="prefetch". Then set the href to the checkout URL: {{ path() }} then name of that route, which is app_checkout.

{% extends 'base.html.twig' %}
{% block metas %}
{{ parent() }}
<link rel="prefetch" href="{{ path('app_checkout') }}">
{% endblock %}
... lines 8 - 32

That's it! By the way, Symfony has a web-link component that can help with this and can even help your server push resources - including CSS and JS files - via a server push. However, when it comes to prefetching another page, I recommend avoiding it and adding the link manually... because pages that are prefetched via the web-link component won't have access to the session cookie.

Anyways, let's go see what happens. Refresh the page and, on the network tools, click to see all types of requests... and scroll to the top. The top request, of course, is for /cart. But now... scroll down... there it is! A request for /checkout that took 360 milliseconds! This happens thanks to the prefetch link we just added. And even though you don't see it here, your browser knows to fetch this with the lowest priority: requests for other things - like CSS and JS files - have a higher priority.

So what happens now when we go to the checkout page? Let's find out: click "Check out"... then scroll back up to the top of the requests. Cool. Turbo - which doesn't know or care that we're doing this prefetch stuff - made its Ajax call like normal. But when it did, our browser was smart enough to instantly pull that from the prefetch cache: no second request was actually made! Instead of waiting 360 milliseconds for the Ajax request to finish and then rendering, Turbo started rendering, effectively, immediately.

Best of Both Worlds?

So this method of manually adding a link tag isn't as fancy as the hover technique we saw earlier. But it also avoids making two requests whenever we click a link. On the negative side, when we go to the cart page, a request will be made for the checkout page regardless of whether or not the user even gets close to clicking the checkout link.

So... neither approach is perfect. Could we... combine the two ideas? Yep! And that's exactly what the data-prefetch-with-link attribute attempts to do: it waits until you hover, and then adds the prefetch link. There are other tiny libraries that do something similar - like "instant.page" and "quicklink" - which makes sense... since adding a prefetch link tag has nothing to do with Turbo.

But... the devil is in the details. Suppose that we use this prefetch script - or one of those other libraries - to dynamically inject a <link rel="prefetch"> into our head element whenever we hover over a link. That will work great. But when we navigate to a new page with Turbo, that <link> tag will not be included on the next page.

Watch: if we click to the cart page, and look in the head... actually, let me refresh to avoid any surprises. Here's the <link rel="prefetch". But now click to another page... then look in the <head>. Uh, duh, I'm still on the cart page - click to the homepage. Now the prefetch link is gone! This is just how Turbo works: when we navigate, the JavaScript and CSS tags inside the head element do persist across pages. But everything else is removed and replaced with whatever is on the new page only.

This has a big impact on prefetch. Our browser did prefetch the checkout page a minute ago when we were on /cart. But because the link tag is gone, our browser basically "forgets" that it did that. In a perfect world, as we navigate with Turbo, any prefetch links that were dynamically added would remain in your head element. That's probably possible by keeping track of all the links that you've prefetched and leveraging a Turbo event listener, but I haven't experimented with it yet. If you do play around with this and get some nice results, I would love to hear about it.

Here's the takeaway: even though these prefetch options are really cool and they can make your site mega fast, none of these are perfect yet. So use them wisely. In the real-world, I would probably use a link-by-link "opt-in" approach with the hover logic that leverages native prefetch links.

Okay: we are done with Turbo Drive! So let's turn to Turbo Frames: a feature that allows us to separate our site into little pieces that can navigate independently.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=7.4.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "1.11.99.1", // 1.11.99.1
        "doctrine/annotations": "^1.0", // 1.13.1
        "doctrine/doctrine-bundle": "^2.2", // 2.3.2
        "doctrine/orm": "^2.8", // 2.9.1
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "sensio/framework-extra-bundle": "^6.1", // v6.1.4
        "symfony/asset": "5.3.*", // v5.3.0-RC1
        "symfony/console": "5.3.*", // v5.3.0-RC1
        "symfony/dotenv": "5.3.*", // v5.3.0-RC1
        "symfony/flex": "^1.3.1", // v1.13.3
        "symfony/form": "5.3.*", // v5.3.0-RC1
        "symfony/framework-bundle": "5.3.*", // v5.3.0-RC1
        "symfony/property-access": "5.3.*", // v5.3.0-RC1
        "symfony/property-info": "5.3.*", // v5.3.0-RC1
        "symfony/proxy-manager-bridge": "5.3.*", // v5.3.0-RC1
        "symfony/runtime": "5.3.*", // v5.3.0-RC1
        "symfony/security-bundle": "5.3.*", // v5.3.0-RC1
        "symfony/serializer": "5.3.*", // v5.3.0-RC1
        "symfony/twig-bundle": "5.3.*", // v5.3.0-RC1
        "symfony/ux-chartjs": "^1.1", // v1.3.0
        "symfony/ux-turbo": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.0-RC1
        "symfony/webpack-encore-bundle": "^1.9", // v1.11.2
        "symfony/yaml": "5.3.*", // v5.3.0-RC1
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.1
        "twig/intl-extra": "^3.2", // v3.3.0
        "twig/string-extra": "^3.3", // v3.3.1
        "twig/twig": "^2.12|^3.0" // v3.3.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.0
        "symfony/debug-bundle": "^5.2", // v5.3.0-RC1
        "symfony/maker-bundle": "^1.27", // v1.31.1
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/stopwatch": "^5.2", // v5.3.0-RC1
        "symfony/var-dumper": "^5.2", // v5.3.0-RC1
        "symfony/web-profiler-bundle": "^5.2", // v5.3.0-RC1
        "zenstruck/foundry": "^1.10" // v1.10.0
    }
}