June 4, 2018











Webpack 4 brought us some changes. Among things like faster bundling, it introduced SplitChunksPlugin, which made CommonsChunksPlugin obsolete. In this article, you will learn how to split your output code to improve the performance of our application.

The idea of code splitting

First things first: what exactly is code splitting in webpack? It allows you to split your code into more than one file. If used correctly, it can improve the performance of your application a lot. Of the reasons for it is the fact, that browsers are caching your code. Every time you make a change, the file containing it has to be re-downloaded by all of the people visiting your site. You probably don’t change your dependencies that much, though. If you split them into a separate file, visitors would not have to download it again then.

Using webpack results in one or more bundles, that contain final versions of our source code. They are composed out of chunks.

Entry

Entry is a definition of a file in our code where the application starts executing, and therefore webpack starts bundling. You can define one entry point (which would happen with Single-Page Application), or multiple entry points (with Multiple-Page Application).

Defining an entry point will result in creating a chunk. If you define just one entry point using a string, it will be named main. If you define more using an object, they will be named after the parameter of the entry object. Examples below are equivalent:

1 entry : './src/index.js'

1 2 3 entry : { main : './src/index.js' }

Output

Output object is a configuration of how and where Webpack should output our bundles and assets. While there can be more than one entry point, only one output configuration is specified. This is where the name of our chunks matter. You can define an exact filename for our bundled output, but since we want to split our code, you shouldn’t do that. You can use [name] to create a template for filenames of our output files:

1 2 3 4 output : { filename : '[name].[chunkhash].bundle.js' , path : path . resolve ( __dirname , 'dist' ) }

One important thing to notice here is [chunkhash]: it is a chunk-specific hash that will be generated based on the contents of your file. It will change only if the content of the file itself changes. It is due to the fact, that browser would otherwise cache it. If the filename changes, the browser will know that it needs to be redownloaded. An example of chunkhash looks like that: 0c553ebfd158e16da428

Our main chunk will be bundled into a file named main.[chunkhash].bundle.js then.

SplitChunksPlugin

Thanks to SplitChunksPlugin, you can move certain parts of your application to separate files. If a module is used in more than one of your chunks, it can be easily shared between them. This is a default behaviour of Webpack.

utilities/users.js

1 2 3 4 5 6 export default [ { firstName : "Adam" , age : 28 } , { firstName : "Jane" , age : 24 } , { firstName : "Ben" , age : 31 } , { firstName : "Lucy" , age : 40 } ]

a.js

1 2 3 4 import _ from 'lodash' ; import users from './users' ; const adam = _ . find ( users , { firstName : 'Adam' } ) ;

b.js

1 2 3 4 import _ from 'lodash' ; import users from './users' ; const lucy = _ . find ( users , { firstName : 'Lucy' } ) ;

webpack.config.js

1 2 3 4 5 6 7 8 9 10 module . exports = { entry : { a : "./src/a.js" , b : "./src/b.js" } , output : { filename : "[name].[chunkhash].bundle.js" , path : __dirname + "/dist" } } ;

If you run it, you can see that webpack created two files: a.[chunkhash].bundle.js and b.[chunkhash].bundle.js and every one of them contains a copy of lodash library: this is not so good! I’ve said before that creating separate files for shared libraries is a default behaviour of webpack, but this concerns async chunks, meaning files that we import asynchronously. We will cover that topic more while describing lazy loading. To involve all types of chunks, we need to change our webpack configuration a bit:

webpack.config.js

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 module . exports = { entry : { a : "./src/a.js" , b : "./src/b.js" } , output : { filename : "[name].[chunkhash].bundle.js" , path : __dirname + "/dist" } , optimization : { splitChunks : { chunks : "all" } } , } ;

Now we can see that additional vendors~a~b.[chunkhash].bundle.js file was created and contains Lodash library. This is thanks to the fact, that by default we have some cacheGroups configuration out of the box:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 splitChunks : { chunks : "all" , cacheGroups : { vendors : { test : / [ \ \ / ] node_modules [ \ \ / ] / , priority : - 10 } , default : { minChunks : 2 , priority : - 20 , reuseExistingChunk : true } } }

First of them are vendors that contain files from your node_modules. Second is a default cache group for all other shared modules. There is one small gotcha here: a redundancy occurred. Both a.[chunkhash].bundle.js and b.[chunkhash].bundle.js contain users.js contents. This is because, by default, SplitChunksPlugin will split chunks only for files bigger than 30Kb. We can easily change that:

webpack.config.js

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 module . exports = { entry : { a : "./src/a.js" , b : "./src/b.js" } , output : { filename : "[name].[chunkhash].bundle.js" , path : __dirname + "/dist" } , optimization : { splitChunks : { chunks : "all" , minSize : 0 } } } ;

This resulted in creating a new file named a~b.[chunkhash].bundle.js which is a default cache group here. Since our users.js file takes a lot less space than 30Kb, it would not be bundled into a separate file without changing the minSize property. In the real-world situation, this is a good thing, because this wouldn’t give us any real performance boost and would force the browser to make an additional request for the utilities.js file which is very small right now.

We can even go a little further and just make an exception for files in the utilities directory:

webpack.config.js

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const HtmlWebpackPlugin = require ( 'html-webpack-plugin' ) ; module . exports = { entry : { a : "./src/a.js" , b : "./src/b.js" } , output : { filename : "[name].[chunkhash].bundle.js" , path : __dirname + "/dist" } , optimization : { splitChunks : { chunks : "all" , cacheGroups : { utilities : { test : / [ \ \ / ] src [ \ \ / ] utilities [ \ \ / ] / , minSize : 0 } } } } } ;

Now our bundle contains 4 files: a.[chunkhash].bundle.js, b.[chunkhash].bundle.js, vendors~a~b.[chunkhash].bundle.js and utilities~a~b.[chunkhash].bundle.js. Even if we would now make minSize: 0 a global setting (in the splitChunks object), the default cache group would not be created. This is because all files that might have been included are covered by the utilities group that we have just created. It has a default priority of 0, which is higher than on default cache group. As you might have already noticed, default cache group has a priority set to -20.

There are other default parameters set for you, which you can check out in the SplitChunksPlugin documentation.

Summary

Even when you have just one entry point (which would happen in most single-page applications) it is a very good idea to keep your dependencies in a separate file. This is actually very simple to achieve because using SplitChunksPlugin is a default behaviour of Webpack 4 and it would probably be enough for you to set chunks: "all" in your splitChunks configuration. If you would like me to cover other aspects of it, let me know. Soon we will also learn how to improve our performance even more with lazy loading, so stay tuned!