Buy

CommonsChunkPlugin: Shared/Vendor Entry

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Restart Webpack!

./node_modules/.bin/webpack --watch

Woh, those JavaScript files are huge! And there's a really simple reason why: jQuery. Yep, login.js imports jquery:

24 lines assets/js/login.js
... lines 1 - 2
import $ from 'jquery';
... lines 4 - 24

rep_log.js imports jquery:

13 lines assets/js/rep_log.js
import $ from 'jquery';
... lines 2 - 13

And layout.js? Yep, it imports jquery too:

12 lines assets/js/layout.js
... lines 1 - 2
import $ from 'jquery';
... lines 4 - 12

That's good! We use it in each module. But, it means that jQuery is packaged in each output file individually. That's super wasteful! Instead, jQuery, well, really any code that's needed on most pages, should probably live in its own file that's included on every page, and removed from everywhere else.

How can we do that? Magic. Or, the CommonsChunkPlugin.

Adding the CommonsChunkPlugin

Open webpack.config.js. Under the plugins section, add new webpack.optimize.CommonsChunkPlugin() and pass that an object with two keys: name set to vendor and minChunks set to 2:

113 lines webpack.config.js
... lines 1 - 30
module.exports = {
... lines 32 - 93
plugins: [
... lines 95 - 105
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: 2,
})
],
... line 111
};

Before we talk about what this does. Try it. Restart Webpack!

./node_modules/.bin/webpack --watch

Woh! There are two really important things! First... a new file! Welcome! Webpack is now outputting vendor.js. And second, layout.js, rep_log.js and login.js are now all much smaller. Heck, login.js is tiny!

This is the power of the CommonsChunkPlugin. Wait, what the heck is a chunk? Webpack uses the word "chunk" a lot... and yet... somehow... nobody can seem to agree on a definition for chunk. But basically, a chunk refers to a bundle of code... in a generic sense. CommonsChunkPlugin has its name because it allows us to move common, shared, code into a separate chunk... i.e. a separate output file.

Thanks to this configuration, whenever Webpack sees a module that is required two or more times, it is put into the vendor.js chunk and removed from all other chunks. Yep, since jquery is imported in all three of these files, Webpack puts it in vendor.js and then does not put it in layout.js, rep_log.js, or login.js.

Including the vendor.js File

But, for this to work, vendor.js needs to be included on every page. Open the base layout: app/Resources/views/base.html.twig. Then, before layout.js, add a script tag for vendor.js:

104 lines app/Resources/views/base.html.twig
... lines 1 - 94
{% block javascripts %}
... lines 96 - 97
<script src="{{ asset('build/vendor.js') }}"></script>
<script src="{{ asset('build/layout.js') }}"></script>
{% endblock %}
... lines 101 - 104

In web/build, open up vendor.js. Yea! You can totally see jQuery right inside. And if you looked at the other built files, you would not find it anymore.

Include everything from node_modules/?

That's amazing right? Let's make our entry files even smaller. Here's another common configuration. Instead of minChunks set to a number, you can pass a callback function with a module argument. I'll paste a bit of code:

115 lines webpack.config.js
... lines 1 - 30
module.exports = {
... lines 32 - 93
plugins: [
... lines 95 - 105
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module) {
return module.context && module.context.indexOf("node_modules") !== -1;
}
})
],
... line 113
};

This simply says: if a module comes from the node_modules/ directory, put it in the vendor.js file. Re-run Webpack now:

./node_modules/.bin/webpack --watch

Wow! The results are super dramatic: rep_log.js is almost empty. But... there's a problem. Do you see it? Configuring the "commons" entry is not an exact science. By blindly including everything from node_modules/, we have probably hurt performance!

How? Well, imagine login.js requires some giant module from node_modules/. Even though this module is only needed for login, it will now be included in vendor.js. That means your users will need to download this giant module just to see the homepage... even though the homepage doesn't use it!

Yep, you need to find a balance between small entry files and a small vendor file, since it's included on every page.

I'll show you my favorite setup next.

Leave a comment!

  • 2018-03-28 Victor Bocharsky

    Hey David,

    Thanks for mentioning it! Yes, exactly what we're going to do. Actually, note about "webpack@3" is already added to this chapter: https://knpuniversity.com/s... . More Webpack4-related notes will be added later. Thanks for your help on it!

    Cheers!

  • 2018-03-27 David Patterson

    @waeverryan,

    So here is something simple that you could add to the existing course at the point where we install webpack:

    yarn add webpack@3 webpack-cli --dev

    You could add it to the comments and add one of those nice little banners that you use in the video itself.

    As I'm sure you're aware, the @3 will cause yarn to install V3. That should remove any hassles for your students until you're ready to release versions for V4.

    Just a thought.

    Thanks!
    Dave
    --

  • 2018-03-27 weaverryan

    Hey David Patterson!

    Great question. At the beginning of the Encore course, we actually *totally* uninstall Webpack before we install Encore (because Encore installs Webpack v3 for you). So yea, basically you'll continue with Webpack V3. But don't worry, you've already tackled (for better or worse) the BIG difference between Webpack v3 and v4 - the common chunk stuff.

    Hopefully, not too long from now, we'll upgrade Encore from Webpack v3 to v4. Most likely, as an Encore user, you won't notice much difference, except that your builds will (supposedly) be faster :).

    Cheers!

  • 2018-03-27 weaverryan

    Hey David Patterson!

    This is GREAT. Nice work, and thank you for sharing! This indeed seems like the "equivalent" for setting up the CommonsChunkPlugin (when you want all of node_modules to be included in it). The next question will be to see if we can make that vendor.js file act like we do in the NEXT chapter: where we make the commons chunk layout.js, and simply prevent anything in layout.js from being in other files (instead of packaging everything in node_modules). It's also possible, considering how much work they've done with the chunk stuff in v4, that thinking about the common chunk in this way is no longer the *best* way. ALL things I need to research soon :).

    Again, thanks for sharing - it's a treat for me when I get to learn from the comments :).

    Cheers!

  • 2018-03-26 David Patterson

    Ryan,

    Since I want to finish this course and proceed to the Webpack/Encore course, should I uninstall webpack 4 and install webpack 3?
    Or, is there a reasonable way to have both of them installed?

    Thanks.

  • 2018-03-26 David Patterson

    weaverryan,

    Thanks for the response.

    I saw @gricard's post over the weekend and didn't really get anything out of it at the time.
    Going back today, I reread it and all of the comments.

    Using his optimization configuration:

      optimization: {
    splitChunks: {
    cacheGroups: {
    commons: {
    test: /[\\/]node_modules[\\/]/,
    name: 'vendor',
    chunks: 'all'
    }
    }
    }
    },

    and placing an additional script tag in the index.html.twig template before the buiild/rep_log.js tag, (as mentioned in the comments), seems to have done the trick.

    <script src="{{ asset('build/vendor.js') }}"></script>
    <script src="{{ asset('build/rep_log.js') }}"></script>

    Thanks

  • 2018-03-26 weaverryan

    Hey David Patterson!

    Unfortunately, I haven't yet worked with Webpack 4 - it's on my "short list" to look at soon (so we can update Webpack Encore), but it hasn't happened yet :/. From my initial reading it seems like the common chunking thing only happens (out of the box) for async chunks - the stuff we cover in the "Code Splitting" chapter. By setting the config to "all" like you did, you ARE activating it for all chunks... but apparently this will require you to add different script tags to your page - Sokra mentions it just a *tiny* bit in the first few sentences: https://gist.github.com/sok.... It appears that the chunking is more automatic, and so the exact script tags you need to have in your HTML source will vary and you need to build the script tags dynamically. That's no small feat.

    Again, I need to look into it more. I believe there is a way to control these filenames - check out this guide: https://gist.github.com/gri... - search for "Let's just try to add the new config for the CommonsChunkPlugin replacement and see what happens". Again, I don't have the answers yet, just trying to give the best direction I can at this time.

    Let me know what you find out - would love to hear! Cheers!

  • 2018-03-26 David Patterson

    Okay. After much digging around, I've realized that the replacement is actually just a new configuration section, specifically optimization.splitChunks.

    I've added this to my webpack.config.js file:

    optimization: {
    splitChunks: {
    chunks: 'all'
    }
    },

    This appears to result in the creation of additional chunks with the common modules:

    $ ./node_modules/.bin/webpack
    Hash: ec802fcda25872729d3a
    Version: webpack 4.2.0
    Time: 4312ms
    Built at: 3/26/2018 9:05:35 AM
    Asset Size Chunks Chunk Names
    layout.js 2.44 MiB layout [emitted] layout
    dumbbell-mini-41a097.png 684 bytes [emitted]
    fontawesome-webfont-912ec6.svg 434 KiB [emitted]
    fontawesome-webfont-b06871.ttf 162 KiB [emitted]
    fontawesome-webfont-af7ae5.woff2 75.4 KiB [emitted]
    fontawesome-webfont-fee66e.woff 95.7 KiB [emitted]
    fontawesome-webfont-674f50.eot 162 KiB [emitted]
    login.js 30.5 KiB login [emitted] login
    rep_log.js 612 KiB rep_log [emitted] rep_log
    vendors~layout~login~rep_log.js 771 KiB vendors~layout~login~rep_log [emitted] vendors~layout~login~rep_log
    vendors~layout~rep_log.js 178 KiB vendors~layout~rep_log [emitted] vendors~layout~rep_log
    static/dumbbell.png 6.66 KiB [emitted]
    Entrypoint rep_log = vendors~layout~login~rep_log.js vendors~layout~rep_log.js rep_log.js
    Entrypoint login = vendors~layout~login~rep_log.js login.js
    Entrypoint layout = vendors~layout~login~rep_log.js vendors~layout~rep_log.js layout.js
    [./assets/css/login.css] 1.1 KiB {login} [built]
    [./assets/css/main.scss] 1.43 KiB {layout} [built]
    [./assets/images/dumbbell-mini.png] 70 bytes {layout} [built]
    [./assets/js/layout.js] 512 bytes {layout} [built]
    [./assets/js/login.js] 977 bytes {login} [built]
    [./assets/js/rep_log.js] 702 bytes {rep_log} [built]
    [./node_modules/css-loader/index.js??ref--5-1!./assets/css/login.css] ./node_modules/css-loader??ref--5-1!./assets/css/login.css 4.68 KiB {login} [built]
    [./node_modules/css-loader/index.js??ref--6-1!./node_modules/resolve-url-loader/index.js??ref--6-2!./node_modules/sass-loader/lib/loader.js??ref--6-3!./assets/css/main.scss] ./node_modules/css-loader??ref--6-1!./node_modules/resolve-url-loader??ref--6-2!./node_modules/sass-loader/lib/loader.js??ref--6-3!./assets/css/main.scss 501 KiB {layout} [built]
    [./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 509 bytes {vendors~layout~rep_log} [built]
    + 364 hidden modules
    $

    The application fails, though with

    Uncaught ReferenceError: $ is not defined at lift:218

    Actually, the app fails in other ways as well. It appears that none of the CSS is delivered to the browser.
    In fact, there are no style CSS link tags in the head.

    As far as I can tell, none of the common js files is ever references in the browser.
    Looking at the network tab in Chrome only shows the FOS router stuff and the app-specific JS files.

    I have spent hours trying to get this to work.

    Advice would be most appreciated.

    Thanks

  • 2018-03-24 David Patterson

    Well rats. WebPack V4 removed CommonsChunkPlugin() and I have been unable to figure out how to get its replacement (SplitChunksPlugin()) to work.

    I've added it to my webpack.config.js file and specified chunks: 'all', but to no apparent avail.
    I have no new output files and my other output files are the same size as before.

    An update here in the Conversation would be most welcome.

    Thanks.