If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeLooking 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.
... 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.
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.
"Houston: no signs of life"
Start the conversation!
// composer.json
{
"require": {
"php": ">=8.1",
"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.18.5
"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/ux-turbo-mercure": "^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
}
}
// package.json
{
"devDependencies": {
"@babel/preset-react": "^7.0.0", // 7.13.13
"@fortawesome/fontawesome-free": "^5.15.3", // 5.15.3
"@hotwired/turbo": "^7.0.0-beta.5", // 1.2.6
"@popperjs/core": "^2.9.1", // 2.9.2
"@symfony/stimulus-bridge": "^2.0.0", // 2.1.0
"@symfony/ux-chartjs": "file:vendor/symfony/ux-chartjs/Resources/assets", // 1.1.0
"@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/Resources/assets", // 0.1.0
"@symfony/ux-turbo-mercure": "file:vendor/symfony/ux-turbo-mercure/Resources/assets", // 0.1.0
"@symfony/webpack-encore": "^1.0.0", // 1.3.0
"bootstrap": "^5.0.0-beta2", // 5.0.1
"chart.js": "^2.9.4",
"core-js": "^3.0.0", // 3.13.0
"jquery": "^3.6.0", // 3.6.0
"react": "^17.0.1", // 17.0.2
"react-dom": "^17.0.1", // 17.0.2
"regenerator-runtime": "^0.13.2", // 0.13.7
"stimulus": "^2.0.0", // 2.0.0
"stimulus-autocomplete": "https://github.com/weaverryan/stimulus-autocomplete#toggle-event-always-dist", // 2.0.0
"stimulus-use": "^0.24.0-1", // 0.24.0-2
"sweetalert2": "^11.0.8", // 11.0.12
"webpack-bundle-analyzer": "^4.4.0", // 4.4.2
"webpack-notifier": "^1.6.0" // 1.13.0
}
}