Webpack 4 — Mysterious SplitChunks Plugin

The official release of Webpack-4 boasts about the proven faster build time (around 98%) and reduced chunk sizes.

However, Webpack authors dropped a bomb in developer community by making one major announcement about the plugin responsible for the Code-Splitting across multiple entries. The documentation on CommonsChunkPlugin has the below notice.

This is my humble attempt to understand and help you with SplitChunksPlugin chunks option by taking a common example.

As an early enthusiast I tried to understand the sorcery behind Code-Splitting. The docs says that splitChunks.chunks accepts ‘initial’, ‘async’ and ‘all’. I was kinda confused, and it raised my curiosity even more!

I dug deeper into the documentation Github history and WebpackOptions Schema and found out that,

“There are 3 values possible ”initial”, ”async” and ”all”. When configured the optimization only selects initial chunks, on-demand chunks or all chunks.” — Github History
“Select chunks for determining shared modules (defaults to “async”, “initial” and “all” requires adding these chunks to the HTML) ”
— WebpackOptions Schema

The idea here is to have 2 entry files a.js and b.js with same node_modules imported. Some of them will be imported dynamically, just to check the behavior of the Code-Splitting.

We’ll use the Webpack Bundle Analyzer Plugin to understand how our modules(node_modules) are being split actually.

a.js :

Only lodash is imported dynamically.

b.js :

React and lodash both are imported dynamically.

The primary reason I chose this configuration was to understand how Webpack config behaves when there is a common lib,

  1. Imported dynamically in one entry and imported non-dynamically - React
  2. Imported dynamically in both entries - lodash
  3. Imported non-dynamically in both entries - jquery

We’ll keep these files untouched and change Webpack config over chunks value.

1. chunks : “async” — Optimization over async module

webpack.config.js
BundleAnalyzer Plugin graph
chunks : ‘async’ tells webpack that,
“Hey, webpack ! I only care about optimization of modules imported dynamically. You can just leave non-dynamic modules as they are.”

Now, let’s see what happened step by step:

  • Webpack takes react from b.js and move it in new file, but keeps react from a.js as it is. As optimizations happens only on the dynamic modules, import("react") statement will cause separate file, while import "react" will not.
  • Webpack takes lodash from a.js and move it in a new file, which is referred by b.js as well.
  • There will be no optimizations for jquery even though it is referenced in both a.js and b.js. Why? (Hint : ‘Async’)

2. chunks : “initial” — Optimization over Sync Module

webpack.config.js
BundleAnalyzer Plugin graph
chunks : ‘initial’ tells webpack that,
“Hey, webpack ! I don’t care about the dynamically imported modules, you can have separate files for each one of them. However, I want all my non-dynamically imported modules in one bundle, although I am ready to share and chunk my non-dynamically imported modules with other files if they also want non-dynamically imported modules.”

Now, let’s see what happened step by step:

  • react from a.js will be moved to node_vendors~a.bundle.js while react from b.js will be moved to its separate bundle 0.bundle.js.
  • lodash from both a.js and b.js will be moved to 1.bundle.js . Why ? It is a dynamically imported module.
  • jquery being a common module which is being imported non-dynamically will be shared between a.js and b.js in node_vendors~a~b.bundle.js.

3. chunks : ‘all’ — Optimization over Async and Sync Module

webpack.config.js
BundleAnalyzer Plugin graph
chunks : ‘all’ tells webpack that,
“Hey, webpack ! I don’t care if it is a dynamically imported module or non-dynamically imported module. Apply optimization over all of them. But make sure that…naah, you are smart enough to do that !”

Now, let’s see what happened step by step:

  • react is non-dynamically imported module in a.js and dynamically imported module in b.js. So, it goes to single file 0.bundle.js,which will be referred by both.
  • lodash is dynamically imported in both, so it obviously gets a separate file 1.bundle.js.
  • jquery is non-dynamically imported in both, so it goes to common shared module node_vendors~a~b.bundle.js,and will be referred by both.

Finally,

Many thanks to various discussions and blog posts of Tobias Koppers and Sean T. Larkin on Github, Medium and other channels to help me writing this.
Thank you Jamund Ferguson for writing this awesome post explaining your journey from RequireJS to Webpack. I would not be able to reach here without that.