Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Optimizing the "Commons" Chunk

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

Here's how I like to configure the CommonsChunkPlugin. Set minChunks to Infinity. Then change the name option to layout:

115 lines webpack.config.js
... lines 1 - 30
module.exports = {
... lines 32 - 93
plugins: [
... lines 95 - 105
new webpack.optimize.CommonsChunkPlugin({
// "layout" is an entry file
// anything included in layout, is not included in other output files
name: 'layout',
minChunks: Infinity
})
],
... line 113
};

That name is important. Scroll up: layout is the name of one of our entries!

115 lines webpack.config.js
... lines 1 - 30
module.exports = {
entry: {
... lines 33 - 34
layout: './assets/js/layout.js',
},
... lines 37 - 93
plugins: [
... lines 95 - 105
new webpack.optimize.CommonsChunkPlugin({
// "layout" is an entry file
// anything included in layout, is not included in other output files
name: 'layout',
minChunks: Infinity
})
],
... line 113
};

Because of this, anything that is required in layout.js will not be included in other files. Yep, since layout.js requires jquery, it will live in the compiled layout.js, but not in login.js or rep_log.js:

... lines 1 - 2
import $ from 'jquery';
import 'bootstrap-sass';
... lines 5 - 12

And since bootstrap-sass is used here, it also won't be output in any other built file.

Our layout.js file now serves two purposes. First, to collect all the common modules that we don't want duplicated across multiple entries. And second, as a way for us to execute any global JavaScript we might have.

Ok, find your Webpack terminal and stop it. Then, clear out the build directory:

rm -rf web/build/*

to delete that vendor.js file. Now, Restart webpack:

./node_modules/.bin/webpack --watch

Yep, no vendor.js. In base.html.twig, we can remove the extra vendor.js script tag:

... lines 1 - 94
{% block javascripts %}
... lines 96 - 97
<script src="{{ asset('build/vendor.js') }}"></script>
... line 99
{% endblock %}
... lines 101 - 104

Try it out: refresh! Yes! Everything works!

The Webpack Manifest

Look inside the built layout.js file. Right on top, you can see the Webpack bootstrap: a collection of functions that the rest of the built code use to help organize all the Webpack module-loading magic. Before we used CommonsChunkPlugin, this appeared at the top of every output file. But now - as you can see in the built login.js - it's not there anymore. Webpack is already smart enough to know that since layout.js is our common chunk and will be included on every page, the Webpack bootstrap code doesn't need to be repeated. Clever Bootstrap!

But... there's one small catch. Isn't there always one small catch? The Webpack bootstrap code includes something called the manifest. Internally, Webpack gives each module a number id, and the manifest contains those ids. Sometimes, those ids change.

Normally, we don't care! These are all internal Webpack details! But... if the module ids change... then the manifest changes... and that means that the contents of layout.js change.

Let me say it a different way: because of the module ids in the manifest, if I make a change to, say, login.js, it may cause the built layout.js file to change. Why is that a problem? Caching.

We're going to talk more about caching and versioning later. But for now, let me just say this: you don't want the contents of a built file to change unless it actually needs to change. Why make your user re-download a fresh layout.js file... if nothing important changed inside it?

Extracting the Manifest

Anyways, the fix is to move that Webpack bootstrap code out of layout.js... so it can happily remain unchanged even when the internal module ids change. To do that, open webpack.config.js and go to the CommonsChunkPlugin. Change the name option to an array, with layout and a new entry called manifest:

119 lines webpack.config.js
... lines 1 - 30
module.exports = {
... lines 32 - 93
plugins: [
... lines 95 - 105
new webpack.optimize.CommonsChunkPlugin({
name: [
... lines 108 - 109
'layout',
... line 111
'manifest'
],
... line 114
})
],
... line 117
};

I'll move my first comment down a bit, and then add a new comment above manifest:

Dumps the manifest in a separate file.

119 lines webpack.config.js
... lines 1 - 30
module.exports = {
... lines 32 - 93
plugins: [
... lines 95 - 105
new webpack.optimize.CommonsChunkPlugin({
name: [
// "layout" is an entry file
// anything included in layout, is not included in other output files
'layout',
// dumps the manifest into a separate file
'manifest'
],
... line 114
})
],
... line 117
};

Let's see what this does! Restart Webpack:

./node_modules/.bin/webpack --watch

Ok, we still have a layout.js file... but now we also have a tiny manifest.js! Thanks to this change, layout.js is just our code. And the tiny manifest.js contains the Webpack bootstrap. If the ids change now, the user will only need to re-download that small file.

Of course, to get this to work... we need to add another script tag! Open base.html.twig. Add a second script tag pointing to manifest.js:

... lines 1 - 94
{% block javascripts %}
... lines 96 - 97
<script src="{{ asset('build/manifest.js') }}"></script>
<script src="{{ asset('build/layout.js') }}"></script>
{% endblock %}
... lines 101 - 104

Phew! I know, that was confusing. But our setup rocks! Thanks to CommonsChunkPlugin, anything in layout.js, will not be duplicated in the other files.

Next! Let's learn about the webpack-dev-server.

Leave a comment!

6
Login or Register to join the conversation
Agnes Avatar
Agnes Avatar Agnes | posted 4 years ago | edited

Here is my config for Webpack 4.


  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        commons: {
          name: 'layout',
          enforce: true
        }
      }
    },
    // dumps the manifest into a separate file
    runtimeChunk: {
      name: "manifest",
    },
  },

This reduces the file sizes but the css can't be found when I go to the login page. And it does not find the css after logging in. Remove the optimization and everything returns to normal. What am I missing?

Reply

Hey Agnes!

Hmm. When you execute webpack, what CSS files does it create? Webpack will split both your Js files *and* your CSS files, which means you will need multiple script tags *and* multiple links tags. So, you may just be missing some link tags because of the CSS splitting.

Cheers!

Reply
Agnes Avatar
Agnes Avatar Agnes | weaverryan | posted 4 years ago | edited

Hey Ryan, you're right, it was not generating the css in build. I wound up adding a section to the group. Not sure if this is correct, by it works :)
`
optimization: {

splitChunks: {
  chunks: 'all',
  cacheGroups: {
    commons: {
      name: 'layout',
      test: 'layout',
      enforce: true
    },
    css: {
      name: 'css',
      test: '/\.css$/',
      enforce: true
    }
  }
},
// dumps the manifest into a separate file
runtimeChunk: {
  name: "manifest",
}

}
`

Reply

Hey Agnes!

If it works, that's something :). This stuff can be confusing. On Webpack Encore, we use a pretty simple configuration:


splitChunks: {
    chunks: 'all'
},
runtimeChunk: 'single'

We basically let Webpack to the splitting for us. We then need to handle figuring out which script/link tags to render (as things are split dynamically).

Cheers!

Reply
GDIBass Avatar
GDIBass Avatar GDIBass | posted 5 years ago | edited

How would this be done on webpack 4?

Edit: I think I got it? Maybe?

Looks like (per comments in the previous step) this feature was moved out of plugins an into an optimization tag. To generate a manifest you have to add the runtimeChunk key with a name. Like this:


    optimization: {
        splitChunks: {
            name: 'layout',
            chunks: 'all',
            minChunks: 2,
        },
        runtimeChunk: {
            name: 'manifest'
        }
    },
Reply

Hey GDIBass!

Hmm, yes, well, at least, *almost*. In updating Encore for Webpack 4, here is our code that does this: https://github.com/symfony/...

The key things are:

a) Create a cache group whose name is the same as your entry file that you want to be the "commons" entry
b) I needed enforce: true as an option and also test, set to the entry name.
c) And yes, good job with the runtimeChunk stuff - you got that right.

I need to get an update ready for this chapter anyways for Webpack 4. So if you have any other questions, let me know!

Cheers!

Reply
Cat in space

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

This tutorial explains the concepts of an old version of Webpack using an old version of Symfony. The most important concepts are still the same, but you should expect significant differences in new versions.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.0",
        "symfony/symfony": "3.3.*", // v3.3.16
        "twig/twig": "2.10.*", // v2.10.0
        "doctrine/orm": "^2.5", // v2.7.0
        "doctrine/doctrine-bundle": "^1.6", // 1.10.3
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.5
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.3
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.4.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.26
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "friendsofsymfony/user-bundle": "^2.0", // v2.1.2
        "doctrine/doctrine-fixtures-bundle": "~2.3", // v2.4.1
        "doctrine/doctrine-migrations-bundle": "^1.2", // v1.3.2
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "friendsofsymfony/jsrouting-bundle": "^1.6" // 1.6.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.6
        "symfony/phpunit-bridge": "^3.0" // v3.3.5
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "dependencies": [],
    "devDependencies": {
        "babel-core": "^6.25.0", // 6.25.0
        "babel-loader": "^7.1.1", // 7.1.1
        "babel-plugin-syntax-dynamic-import": "^6.18.0", // 6.18.0
        "babel-preset-env": "^1.6.0", // 1.6.0
        "bootstrap-sass": "^3.3.7", // 3.3.7
        "clean-webpack-plugin": "^0.1.16", // 0.1.16
        "copy-webpack-plugin": "^4.0.1", // 4.0.1
        "core-js": "^2.4.1", // 2.4.1
        "css-loader": "^0.28.4", // 0.28.4
        "extract-text-webpack-plugin": "^3.0.0", // 3.0.0
        "file-loader": "^0.11.2", // 0.11.2
        "font-awesome": "^4.7.0", // 4.7.0
        "jquery": "^3.2.1", // 3.2.1
        "lodash": "^4.17.4", // 4.17.4
        "node-sass": "^4.5.3", // 4.5.3
        "resolve-url-loader": "^2.1.0", // 2.1.0
        "sass-loader": "^6.0.6", // 6.0.6
        "style-loader": "^0.18.2", // 0.18.2
        "sweetalert2": "^6.6.6", // 6.6.6
        "webpack": "^3.4.1", // 3.4.1
        "webpack-chunk-hash": "^0.4.0", // 0.4.0
        "webpack-dev-server": "^2.6.1", // 2.6.1
        "webpack-manifest-plugin": "^1.2.1" // 1.2.1
    }
}
userVoice