webpack: Unraveling CommonsChunkPlugin

Update: With webpack v4.x.x, CommonChunkPlugin has been deprecated in favor of SplitChunksPlugin.

In a separate series, webpack By Example: Part 1, we build up a fairly sophisticated webpack configuration through a series of examples. In the last article in the series I introduced CommonChunkPlugin; however I have since realized that using it is more complex than I originally thought.

The examples’ (along with the ones from the separate series) configuration files and source code is available as a GitHub repository.

analyzer

While I had heard of webpack-bundle-analyzer, I thought it was gimmicky; however, I have since found it extremely useful in troubleshooting webpack bundles (used it heavily in writing this article).

As a reminder, the final example in the series had the following modules and dependencies:

babel-polyfill (vendor multi-main entry)
sillyname (vendor multi-main entry)
src (main entry)
+ babel-polyfill
+ src/cat (dynamic)
+ sillyname
+ src/dog (dynamic)
+ sillyname

note: vendor is example of a multi-main entry; while technically two separate entry points, they will be combined into a single bundle. On the other hand, src/cat and src/dog are examples of dynamic imports which gets split out into it’s own bundle.

Starting from the final example in the series; from the command line in the project directory we execute.

yarn add --dev webpack-bundle-analyzer

We then update webpack.config.js:

webpack.config.js

...
const CleanWebpackPlugin = require('clean-webpack-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
...
new webpack.optimize.CommonsChunkPlugin({
name: ['vendor', 'manifest'],
minChunks: Infinity,
}),
new BundleAnalyzerPlugin(),
...

From the command line in the project directory we execute.

yarn build

We now get a nice visual representation of the bundles (and their contents).

Image for post
Image for post

While it is difficult to see from this screenshot (the actual output is interactive), the bundles and their component modules are:

vendor
+ babel-polyfill
+ sillyname
main
+ src
1
+ src/cat
0
+ src/dog
manifest

Under the Hood

The relevant lines from webpack.config.js are:

webpack.config.js

...
new webpack.optimize.CommonsChunkPlugin({
name: ['vendor', 'manifest'],
minChunks: Infinity,
}),
...

What happens if we remove vendor from CommonChunkPlugin configuration and build?

vendor
+ babel-polyfill
+ sillyname
main
+ src
+ babel-polyfill
1
+ src/cat
+ sillyname
0
+ src/dog
+ sillyname
manifest

What is not obvious here is how is vendor in the CommonChunkPlugin is working to move modules out of main, 1, and 0, when the minChunks is set to Infinity.

Passing `Infinity` just creates the commons chunk, but moves no modules into it.

— webpack

My best interpretation is that vendor is a special case (multi-main entry point).

One problem with this approach, however, is that we had to explicitly specify the vendor dependencies in webpack.config.js. Another one is that we, ideally, do not weigh down the vendor bundle with the sillyname module (not needed in the initial load).

min-chunks

In this example, we will get rid of the vendor multi-main entry and use CommonChunkPlugin in another way to attempt to achieve the same (or better) effect of creating the vendor bundle; the modules and dependencies are:

src (main entry)
+ babel-polyfill
+ src/cat (dynamic)
+ sillyname
+ src/dog (dynamic)
+ sillyname

The changes to webpack.config.js are:

webpack.config.js

...
devtool: env === 'production' ? 'source-map' : 'cheap-eval-source-map',
entry: './src/index.js',
output: {
...
new CleanWebpackPlugin(['dist']),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: ({ resource }) => (
resource !== undefined &&
resource.indexOf('node_modules') !== -1
),
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity,
}),

new BundleAnalyzerPlugin(),
...

In this case, we use a function as a value for minChunks; returning true for modules to be included in the vendor bundle (triggered off a condition on the resource — path to module).

The bundles and component modules in this case are:

vendor
+ babel-polyfill
main
+ src
1
+ src/cat
+ sillyname
0
+ src/dog
+ sillyname
manifest

The single entry point is src (main entry) and looking at the direct children the only module that matches the function criteria is babel-polyfill; sillyname is a direct child of src/cat and src/dog. The result is that vendor has babel-polyfill and not sillyname in it.

children

In this example, we will try something different; the changes from the last example to webpack.config.js are:

webpack.config.js

...
new CleanWebpackPlugin(['dist']),
new webpack.optimize.CommonsChunkPlugin({
name: 'main',
children: true,
minChunks: ({ resource }) => (
resource !== undefined &&
resource.indexOf('node_modules') !== -1
),
})
...

The bundles and component modules in this case are:

main
+ src
+ babel-polyfill
+ sillyname
1
+ src/cat
0
+ src/dog
manifest

The result of specifying main and children is to move any modules in main’s children (dynamic imports) bundles (1 and 0) into the parent.

note: Not sure I understand why, but explicitly referencing main is required.

async

In this example, we, yet again, will try something different; the changes from the last example to webpack.config.js are:

webpack.config.js

...
children: true,
async: true,
minChunks: ({ resource ]) => (
...

The bundles and component modules in this case are:

main
+ src
+ babel-polyfill
0
+ sillyname
2
+ src/cat
1
+ src/dog
manifest

The result of adding async is to instead move any modules in main’s children (dynamic imports) bundles (2 and 1) into a separate bundle (0) that is dynamically (asynchronously) loaded.

Except for babel-polyfill being in the main bundle, we are doing better than our first example; sillyname is only loaded when it is needed by src/cat and src/dog.

min-chunks-async

Finally, we combine our techniques with the following webpack.config.js.

webpack.config.js

...
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: ({ resource }) => (
resource !== undefined &&
resource.indexOf('node_modules') !== -1
),
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'main',
children: true,
async: true,
minChunks: ({ resource }) => (
resource !== undefined &&
resource.indexOf('node_modules') !== -1
),
}),

...

The bundles and component modules in this case are:

vendor
+ babel-polyfill
main
+ src
0
+ sillyname
2
+ src/cat
1
+ src/dog
manifest

We now have a better solution with the static vendor bundle containing all of the third-party modules (babel-polyfill) that are needed by modules (src) in the initial bundle. We are also dynamically loading bundles with the third-party modules (sillyname) need by modules (src/cat and src/dog) in the children dynamic bundles.

Wrap Up

As you can see, using CommonChunksPlugin is not as straightforward as one would hope; but with the patterns we explored here I am more confident that we can more effectively bundle future applications.

Addendum

Shortly after I finished this series, webpack released a new major (3.x.x) version. As such, I updated the last example in this article with it.

Also, I neglected to include the functionality to push the production or development build environments into the application itself. This is important as some frameworks, e.g., React, use the global JavaScript variable process.env.NODE_ENV to trigger different behaviors (e.g., production vs. development).

The fix is to pass a NODE_ENV variable to webpack and then have it pass it down to the application itself.

./v3-update/package.json

...
"scripts": {
"build": "./node_modules/.bin/webpack -p --env.NODE_ENV=production",
"start": "./node_modules/.bin/webpack-dev-server --env.NODE_ENV=development --open",
"start-production": "./node_modules/.bin/webpack-dev-server --env.NODE_ENV=production --open",
"test": "echo \"Error: no test specified\" && exit 1"
},
...

./v3-update/webpack.config.js

module.exports = function(env) {
return ({
devtool: env.NODE_ENV === 'production' ? 'source-map' : 'cheap-eval-source-map',
entry: './src/index.js',
output: {
filename: env.NODE_ENV === 'production' ? '[name].[chunkhash].bundle.js' : '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
...
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(env.NODE_ENV),
}),

new UglifyJSPlugin({sourceMap: true}),
...

We then can now use the variable in our source code.

./v3-update/src/index.js

import 'babel-polyfill';
import './index.css';

Another Addendum

When I went to use this configuration in a project, I realized that I did not have a favicon supported. The quick fix was to install copy-webpack-plugin.

yarn add copy-webpack-plugin --dev

Then place a favicon.ico file in the public folder.

Then update the webpack configuration:

./v3-update/webpack.config.js

...
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
...
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity,
}),
new CopyWebpackPlugin([
{ from: 'public/favicon.ico' },
]),

Written by

Broad infrastructure, development, and soft-skill background

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store