Magic: Lazy-Loading UX Controllers

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

If we look at the current analyzer, it's pretty obvious what our biggest problem is: chart.js is gigantic. That's included because the ux-chartjs controller imports chart.js.

And... what a shame! We only use this controller on an admin page. Yet, every user has to download it no matter what page they go to. Could we... somehow only download that code if a user goes to the admin page?

Actually... we can do something even cooler. We can set things up so that the JavaScript for the controller - including chart.js - is only downloaded - and automatically downloaded - when a data-controller element for that controller appears on the page. It's... magic.

Making a UX Controller Lazy: fetch=lazy

Open assets/controllers.json. This file is automatically updated by Symfony Flex whenever we download a new UX package. But we are totally free to tweak this. For example, we could change enable to false if we didn't need that controller. Or we could change fetch to lazy.

{
"controllers": {
"@symfony/ux-chartjs": {
"chart": {
... line 5
"fetch": "lazy"
}
}
},
... line 10
}

What does that do? At your terminal, hit Ctrl+C to stop the analyzer and let's dump a new stats file. But this time, I'm going to switch to do a dev build... just because it's a little bit faster... though less realistic than profiling a production build:

yarn run --silent dev --json > stats.json

When that finishes, which will still take a little bit of time, run the analyzer again:

yarn webpack-bundle-analyzer stats.json public/build

And... woh! Awesome! All the chart.js code - including the controller - now lives in its own file! Thanks to this, the other files are much smaller.

Let's close a few tabs... click to go to the homepage and then view the page source. Check out the file names on the script tags... Yeah! chart.js is not being loading! The only files that are loaded are runtime.js, app.js and this now, pretty small, long vendors filename. You can also see this if you go to the "Network" tools and filter for JavaScript. The long chart.js filename is not in this list! It is not being downloaded at all.

Now go to /admin. There's our chart! How the heck did that just happen? I thought the chart.js code wasn't being downloaded!

How does Lazy Fetching Work?

When you set fetch to lazy, instead of importing the real controller and its dependencies, the stimulus-bridge library creates a tiny, "fake" controller. That controller waits for a data-controller element matching the controller name to be added to the page. As soon as it sees one, it asynchronously downloads the real controller and executes it. That's bonkers!

Let me show you how amazing this is. Find the canvas element that holds the data-controller attribute, right click and go to "copy outer HTML".

Head to the homepage. As we saw a minute ago, the chart.js code is not loaded on this page at all. Let's clear out the Network tab under JavaScript. Now, inspect element on the h1 - though the location doesn't matter - edit this div's HTML and paste the data-controller element onto the page.

Ready? Click outside of this to activate that. Boom! We see the graph! And check out the "Network" tab. Woh! It asynchronously loaded our chart.js JavaScript code! For performance reasons, it even split moment.js into a second file and downloaded that at the same time!

This is the beauty of the lazy fetch. You can add a data-controller element to any page - or even load that HTML via Ajax - and your Stimulus code will work. If the controller code hasn't actually been downloaded yet because you decided to make it lazy, no problem! It will be downloaded automatically.

But what about our own custom controller code? Back on the analyzer tab, one of the biggest modules left is sweetalert... which we only use on one page for our submit-confirm controller. Could we somehow only download that code when an element with data-controller="submit-confirm" appears on the page? We can! Let's find out how next.

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.11.1
        "doctrine/doctrine-bundle": "^2.2", // 2.2.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
        "doctrine/orm": "^2.8", // 2.8.1
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "sensio/framework-extra-bundle": "^5.6", // v5.6.1
        "symfony/asset": "5.2.*", // v5.2.3
        "symfony/console": "5.2.*", // v5.2.3
        "symfony/dotenv": "5.2.*", // v5.2.3
        "symfony/flex": "^1.3.1", // v1.12.1
        "symfony/form": "5.2.*", // v5.2.3
        "symfony/framework-bundle": "5.2.*", // v5.2.3
        "symfony/property-access": "5.2.*", // v5.2.3
        "symfony/property-info": "5.2.*", // v5.2.3
        "symfony/proxy-manager-bridge": "5.2.*", // v5.2.3
        "symfony/security-bundle": "5.2.*", // v5.2.3
        "symfony/serializer": "5.2.*", // v5.2.3
        "symfony/twig-bundle": "5.2.*", // v5.2.3
        "symfony/ux-chartjs": "^1.1", // v1.2.0
        "symfony/validator": "5.2.*", // v5.2.3
        "symfony/webpack-encore-bundle": "^1.9", // v1.11.1
        "symfony/yaml": "5.2.*", // v5.2.3
        "twig/extra-bundle": "^2.12|^3.0", // v3.2.1
        "twig/intl-extra": "^3.2", // v3.2.1
        "twig/twig": "^2.12|^3.0" // v3.2.1
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.0
        "symfony/debug-bundle": "^5.2", // v5.2.3
        "symfony/maker-bundle": "^1.27", // v1.30.0
        "symfony/monolog-bundle": "^3.0", // v3.6.0
        "symfony/stopwatch": "^5.2", // v5.2.3
        "symfony/var-dumper": "^5.2", // v5.2.3
        "symfony/web-profiler-bundle": "^5.2" // v5.2.3
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@babel/preset-react": "^7.0.0", // 7.12.13
        "@popperjs/core": "^2.9.1", // 2.9.1
        "@symfony/stimulus-bridge": "^2.0.0", // 2.1.0
        "@symfony/ux-chartjs": "file:vendor/symfony/ux-chartjs/Resources/assets", // 1.1.0
        "@symfony/webpack-encore": "^1.0.0", // 1.0.4
        "bootstrap": "^5.0.0-beta2", // 5.0.0-beta2
        "core-js": "^3.0.0", // 3.8.3
        "jquery": "^3.6.0", // 3.6.0
        "react": "^17.0.1", // 17.0.1
        "react-dom": "^17.0.1", // 17.0.1
        "regenerator-runtime": "^0.13.2", // 0.13.7
        "stimulus": "^2.0.0", // 2.0.0
        "stimulus-autocomplete": "^2.0.1-phylor-6095f2a9", // 2.0.1-phylor-6095f2a9
        "stimulus-use": "^0.24.0-1", // 0.24.0-1
        "sweetalert2": "^10.13.0", // 10.14.0
        "webpack-bundle-analyzer": "^4.4.0", // 4.4.0
        "webpack-notifier": "^1.6.0" // 1.13.0
    }
}