Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

The Magic of Split Chunks

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

View the HTML source and search for app.js. Surprise! We have multiple script tags! Actually, let me go to the inspect - it's a bit prettier. Black magic! We have two script tags - one for app.js but also one for vendors~app.js. What the heck? Go look at the public/build/ directory. Yeah, there is a vendors~app.js file.

I love this feature. Check out webpack.config.js. One of the optional features that came pre-enabled is called splitEntryChunks():

... lines 1 - 2
Encore
... lines 4 - 23
// When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
.splitEntryChunks()
... lines 26 - 66
;
... lines 68 - 69

Here's how it works. We tell Webpack to read app.js, follow all the imports, then eventually create one app.js file and one app.css file. But internally, Webpack uses an algorithm that, in this case, determines that it's more efficient if the one app.js file is split into two: app.js and vendors~app.js. And then, yea, we need two script tags on the page.

The Logic of Splitting

That may sound... odd at first... I mean, isn't part of the point of Webpack to combine all our JavaScript into a single file so that users can avoid making a ton of web requests?

Yes... but not always. The vendors~app.js file has some Webpack-specific code on top, but most of this file contains the vendor libraries that we imported. Stuff like bootstrap & jquery.

When Webpack is trying to figure out how to split the app.js file, it looks for code that satisfies several conditions. For example, if it can find code from the node_modules/ directory and that code is bigger than 30kb and splitting it into a new file would result in 3 or fewer final JavaScript files for this entry, it will split it. That's exactly what's happening here. Webpack especially likes splitting "vendor" code - that's the stuff in node_modules/ - into its own file because vendor code tends to change less often. That means your user's browser can cache the vendors~app.js file for a longer time... which is cool, because those files tend to be pretty big. Then, the app.js file - which contains our code that probably changes more often, is smaller.

The algorithm also looks for code re-use. Right now, we only have one entry. But in a little while, we're going to create multiple entries to support page-specific CSS and JavaScript. When we do that, Webpack will automatically start analyzing which modules are shared between those entries and isolate them into their own files. For example, suppose our get_nice_message.js file is imported from two different entries: app and admin. Without code splitting, that code would be duplicated inside the final built app.js and admin.js. With code splitting, that code may be split into its own file. I say "may" because Webpack is smart: if the code is tiny, splitting it into its own file would be worse for performance.

SplitChunksPlugin

All of this craziness happens without us even knowing or caring. This feature comes from a part of Webpack called the SplitChunksPlugin. On top, it explains the logic it uses to split. But you can configure all of this.

Oh, see this big example config? This is a small piece of what Webpack's config normally looks like without Encore: your webpack.config.js would be a big config object like this. So, if we wanted to apply some of these changes, how could we do that in Encore?

The answer lives at the bottom of webpack.config.js. At the end, we call Encore.getWebpackConfig(), which generates standard Webpack config:

... lines 1 - 68
module.exports = Encore.getWebpackConfig();

If you need to, you can always set this to a variable, modify some keys, then export the final value when you're finished:

// webpack.config.js

// ...

const config = Encore.getWebpackConfig();
config.optimization.splitChunks.minSize = 20000;

module.exports = config;

But for most things, there's an easier way. In this case, you can say .configureSplitChunks() and pass it a callback function. Encore will pass you the default split chunks configuration and then you can tweak it:

// webpack.config.js

// ...

Encore.
    // ...
    .splitEntryChunks()
    .configureSplitChunks(function(splitChunks) {
        splitChunks.minSize = 20000;
    })
    // ...
;

module.exports = Encore.getWebpackConfig();

This is a common way to extend things in Encore.

But... Webpack does a pretty great job of splitting things out-of-the-box. And... if you look at the entrypoints.json file, Encore makes sure that this file stays up-to-date with exactly which script and link tags each entry requires. The Twig helpers are already reading this file and taking care of everything:

<!doctype html>
<html lang="en">
... lines 3 - 17
<body>
... lines 19 - 90
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
{% endblock %}
</body>
</html>

Basically, code splitting is free performance.

Oh, and all of this applies to CSS too. In a few minutes, after we've made our CSS a bit fancier, you'll notice that we'll suddenly have multiple link tags.

Next, let's do that! Let's take our CSS up a level by removing the extra link tags from our base layout and putting everything into Encore. To do this, we'll start importing CSS files from third-party libraries in node_modules/.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial works great with Symfony5!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "aws/aws-sdk-php": "^3.87", // 3.91.4
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.1
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.9.0
        "league/flysystem-aws-s3-v3": "^1.0", // 1.0.22
        "league/flysystem-cached-adapter": "^1.0", // 1.0.9
        "liip/imagine-bundle": "^2.1", // 2.1.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.1.0
        "oneup/flysystem-bundle": "^3.0", // 3.0.3
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.3.1
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.2.5
        "symfony/console": "^4.0", // v4.2.5
        "symfony/flex": "^1.9", // v1.17.6
        "symfony/form": "^4.0", // v4.2.5
        "symfony/framework-bundle": "^4.0", // v4.2.5
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.2.5
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "^4.0", // v4.2.5
        "symfony/validator": "^4.0", // v4.2.5
        "symfony/web-server-bundle": "^4.0", // v4.2.5
        "symfony/webpack-encore-bundle": "^1.4", // v1.5.0
        "symfony/yaml": "^4.0", // v4.2.5
        "twig/extensions": "^1.5" // v1.5.4
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.1.0
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.2.5
        "symfony/dotenv": "^4.0", // v4.2.5
        "symfony/maker-bundle": "^1.0", // v1.11.5
        "symfony/monolog-bundle": "^3.0", // v3.3.1
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.2.5
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "^3.3|^4.0" // v4.2.5
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@symfony/webpack-encore": "^0.27.0", // 0.27.0
        "autocomplete.js": "^0.36.0",
        "autoprefixer": "^9.5.1", // 9.5.1
        "bootstrap": "^4.3.1", // 4.3.1
        "core-js": "^3.0.0", // 3.0.1
        "dropzone": "^5.5.1", // 5.5.1
        "font-awesome": "^4.7.0", // 4.7.0
        "jquery": "^3.4.0", // 3.4.0
        "popper.js": "^1.15.0",
        "postcss-loader": "^3.0.0", // 3.0.0
        "sass": "^1.29.0", // 1.29.0
        "sass-loader": "^7.0.1", // 7.3.1
        "sortablejs": "^1.8.4", // 1.8.4
        "webpack-notifier": "^1.6.0" // 1.7.0
    }
}
userVoice