gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
This is Turbo Drive. And yes, it feels like absolute magic. So let's break down how this works.
To start... we never wrote any JavaScript that said:
Hey Turbo! Please activate your Drive functionality.
So... how would did this automatically start working? That is thanks to the magic of the assets/controllers.json
file. This is normally a mechanism in Symfony UX for third party libraries to add new Stimulus controllers to our app. And in this case, that's true... but it's kind of a trick.
Let's go find the file that's being referenced. It lives in node_modules/@symfony/ux-turbo
then src/turbo_controller.js
. If you're wondering how I knew to open this exact file... this turbo-core
string here matches up with a special key inside of the package.json
file of this library. So turbo-core
points to dist/turbo_controller.js
. So, technically the file in the dist/
folder is loaded... but I'm opening the original in src/
because it's a bit easier to read.
And... there's not much here! This exposes an empty controller. And really, the whole point of this file is to import Turbo
and set it onto the window
variable. This accomplishes two things. First, when you import Turbo
, it automatically activates Turbo Drive across your entire site. We'll talk about how to disable it globally or selectively a bit later. And second, Turbo is set onto the window
variable, which makes it a global variable. You may or may not need this. It's useful if you need to programmatically visit a link, but from outside a JavaScript file. We'll see that later.
So we now know who activated Turbo. But... how the heck does Turbo Drive work? It's a pretty simple idea. Turbo watches link clicks - and also form submits like this add to cart form submit - and intercepts them. It then performs those requests in the background via an Ajax call, which we can see here. When that finishes, it updates the HTML of the page from the HTML in the response... all without a full refresh. But, it does modify the URL, which gives us normal browser behavior, like clicking back and forward.
Speaking of back and forward, Turbo Drive has a feature called "snapshots". Let me refresh the page real quick. As you navigate to a new page, it stores a "snapshot" of the page you're leaving. Then, if you click back in your browser, it instantly loads that snapshot with no network request. It does the same if you go forward. And if you revisit a page that you've already been to, so, a page whose snapshot has been stored, Turbo will give you an instant "preview" of that page while it waits for the Ajax call for that page to finish. You can see how super fast the pages are that we've already gone to versus ones that we have not gone to yet. By the way, this snapshot cache isn't persistent: it clears when you refresh the page.
Some of this preview & snapshot stuff is kind of hard to see because things are so fast. So in your editor, open up public/index.php
and add a sleep()
for two seconds.
use App\Kernel; | |
sleep(2); | |
require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; | |
... lines 7 - 10 |
Now head back to your browser and refresh the page... which takes 2 seconds. Click back to the homepage. Oh! This shows off the progress bar! If an Ajax call takes longer than 500 milliseconds, the progress bar shows up, which you can customize with CSS if you want. Because our site is slow, we see it each time we click to a new page.
But now, let's click back to "Office Supplies", which we visited before. When I do, watch closely: the page will show up instantly. Boom! Then it finishes loading the Ajax call. This is the preview feature. When you navigate to a page you've already been to, Turbo loads the page from cache for an instant experience. But it still makes an Ajax call for the page. And when that finishes, it takes the new HTML and renders it onto the page. Most of the time - like right now - we don't really notice that Ajax call finishing... because the new HTML is identical to the preview.
And if we click backward and forward, as we mentioned earlier, those pages load instantly with no Ajax request. Let's go take out that sleep
.
Okay... but how does this all really work? What is Turbo doing behind the scenes to make it all happen? Let's go a step deeper. This is important because, to get Drive working happily on your site, as the saying goes, the devil is in the details. We'll spend the first part of this tutorial talking about potential problems that Turbo Drive can cause and how to fix them.
Let me refresh the page again to clear the snapshot cache.
Okay: when we click a link, Turbo intercepts that and makes an Ajax call instead. Oh, by the way, these extra Ajax requests are for the web debug toolbar.
Anyways, the Ajax request that Turbo makes when we click returns a full HTML page. When Turbo gets that full HTML response, it merges the new head
tag into the existing head
tag and then replaces the body
with the new body
.
The way it merges the old and new head
is smart. Go over to the Elements part of the debugging tools and open up the head
tag.
When the Ajax request finishes, Turbo first finds anything in the head
other than JavaScript and CSS elements and removes those. Then it looks in the new head
for any non JavaScript and non CSS elements and adds those.
We can actually see this. Reload the page and look back at the head
. I see two non-JavaScript and non-CSS tags: a meta
tag with the charset
and the title
element. When I click to go to another page, these will be removed. Then, any elements from the new page's head
will be added to the bottom. And... boom! The new page happens to have the same two tags, but you can see that the original ones were removed and the new ones added. I was lazy and didn't give each page a unique title, but if the next page did have a new title, it would show up.
Let's talk about how JavaScript and CSS is handled. If the new head
tag contains JavaScript or CSS tags - and it probably will, since we're returning full HTML pages - Turbo checks to see if these elements already exist in the current head
. If they do - like the next page we click also has a script tag for build/runtime.js
- then Turbo ignores it. There's no reason to add the same script
or CSS multiple times. But if the CSS or JavaScript element does not exist on the current page, it will add it. This is actually a big reason why Turbo Drive feels so fast: each time you navigate, your browser does not need to re-parse all of your JavaScript and CSS like it normally would with a traditional full page reload.
The result of all of this is... exactly what we see as we click around! The title changes on each page - the login page has a different title - and if a page contained new JavaScript or CSS, that would be added automatically.
So... this is amazing! Well, yes, it is amazing. But to get this amazingness to work perfectly, there is a little bit of work that we need to do. The first bit involves making your JavaScript Turbo-friendly. Let's dive into that topic next.
Hey Nick F.!
It shouldn't, and there are a few reasons. First, bots like Google now support JavaScript - so they are able to crawl even full SPA's (e.g. written in React). Second, if a bot *didn't* support running JavaScript, then Turbo wouldn't be active and each "click" on your site would be a normal full page refresh, and so that would work perfectly too. And third, in case it helps you be more confident, Turbo (as Turbolinks) has been around for *years* and I can't find anyone complaining about SEO problems after switching to it.
Cheers!
// 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
}
}
So I'm using "bootstrap": "^5.1.3" and using a navbar Offcanvas and I get a backdrop being added multiple times, like the JS is being executed every time (as you mentioned in a previous video) I change page with turbo (backdrop continually gets darker) and it will clear if I do a full page reload. How can this be fixed?