webpack bits: Getting the most out of the CommonsChunkPlugin()

From time to time, the webpack core team loves to get the community involved on Twitter, and share bits and pieces of knowledge in a fun and informative way.

This time, the “rules to the game” were simple. Install webpack-bundle-analyzer, generate a fancy colorful image of all of your bundles, and share it with me. In return, the webpack team offered to help identify any potential issues we could spot!

What did we find?

The most common theme was code duplication: Libraries, components, code was duplicated across multiple [sync or async] bundles!

Case One: Many vendor bundles with duplicate code

This is a specimen example. Thank you Swizec for letting me share it.

Swizec Teller was kind enough to share one of his builds (which in fact is built for over 8–9 standalone single-page applications ). I chose this example out of all of them because there so many great techniques we can identify from it. So lets look at this in more detail:

Closest to the “FoamTree” icon is the application code itself, meanwhile, anything that was used from node_modules is to the far left ending in “_vendor.js”

We can infer quite a few things from this (without looking at the actual configuration).

Each single-page app is using a new CommonsChunkPlugin that targets just that entry point, and its vendor code. This creates a bundle with only modules that come from node_modules folder, and another bundle with just application code. The configuration portion was even provided:

Object.keys(activeApps)
.map(app => new webpack.optimize.CommonsChunkPlugin({
name: `${app}_vendor`,
chunks: [app],
minChunks: isVendor
}))

The activeApps variable most likely represents each of the individual entry points.

Areas of Opportunity

Below are a few areas that I circled that could use some improvement.

“Meta” caching

What we see above is many large libraries like momentjs, lodash, and jquery being used across 6 or more vendor bundles. The strategy for adding all vendors into a separate bundle is good, but we should also apply that same strategy across all vendor bundles.

I suggested that Swizec add the following at the end of his plugins array:

new webpack.optimize.CommonsChunkPlugin({
children: true,
minChunks: 6
})

What we are telling webpack is the following:

Hey webpack, look across all chunks (including the vendor ones that were generated) and move any modules that occur in at least 6 chunks to a separate file.
In this case it looks like the file was named “manifest.js”?

As you can see now, all of those modules were extracted into a separate file, and on top of that, Swizec reported that this decreased overall application sizes by 17%!

Case Two: Duplicate vendors across async chunks:

This is, in fact, a very impressive use of code splitting. And look at all the pretty colors 💓

So this amount of duplication wasn’t as severe in terms of overall code size, however, when you look at the full size image below, you can see the exact same 3 modules across every async chunk.

Async chunks are ones containing “[number].[number].js” in their filename

As you can see above, the same 2–3 components are used across all 40–50 async bundles. So how do we solve this with CommonsChunkPlugin?

Create an async Commons Chunk

The technique will be very similar to the first, however we will need to set the async property in the configuration option, to true as seen below:

new webpack.optimize.CommonsChunkPlugin({
async: true,
children: true,
filename: "commonlazy.js"
});

In the same way — webpack will scan all chunks and look for common modules. Since async: true, only code split bundles will be scanned. Because we did not specify minChunks the value defaults to 3. So what webpack is being told is:

Hey webpack, look through all normal [aka lazy loaded] chunks, and if you find the same module that appears across 3 or more chunks, then separate it out into a separate async commons chunk.

Here is what the result was:

There is likely room to even play with a larger minChunks value here to result in a smaller commonlazy.js bundle.

Now the async chunks are extremely tiny, and all of that code has been aggregated into one file called commonlazy.js . Since these bundle were already pretty tiny, the size impact wasn’t very noticeable until second visit. Now there is far less data being shipped per code split bundle and we are saving users load time and data consumption by placing those common modules into a separate cacheable chunk.

More control: minChunks function

So what if you want to have more control? There are scenarios where you don’t want to have a single shared bundle because not every lazy/entry chunk may use it. The minChunks property also takes a function!! This can be your “filtering predicate” for what modules are added to your newly created bundle. Below are examples of

new webpack.optimize.CommonsChunkPlugin({
filename: "lodash-moment-shared-bundle.js",
minChunks: function(module, count) {
return module.resource && /lodash|moment/.test(module.resource) && count >= 3
}
})

The example above says:

Yo webpack, when you come across a module whos absolute path matches lodash or momentjs, and occurs across 3 separate entries/chunks, then extract those modules into a separate bundle.

You could apply this same behavior to async bundles by setting `async: true` also!

Even moar control

With this minChunks you can create smaller subsets of cacheable vendors for specific entries and bundles. In the end, you may wind up with something that looks like this:

function lodashMomentModuleFilter(module, count) {
return module.resource && /lodash|moment/.test(module.resource) && count >= 2;
}
function immutableReactModuleFilter(module, count) {
return module.resource && /immutable|react/.test(module.resource) && count >=4
}
new webpack.optimize.CommonsChunkPlugin({
filename: "lodash-moment-shared-bundle.js",
minChunks: lodashMomentModuleFilter
})
new webpack.optimize.CommonsChunkPlugin({
filename: "immutable-react-shared-bundle.js",
minChunks: immutableReactModuleFilter
})
EDIT (April 1st): I stated that you could use filename with minChunks, but we prevent this now as of webpack 2.3.2+.

There is no silver bullet!

CommonsChunkPlugin() may be powerful, but keep in mind that each one of these examples is tailored to the application it is applied to. So before you copy-pasta these snippets in, take advice from Sam Saccone and Paul Irish and MPDIA first to make sure you apply the right solution.

Always understand your process before applying solutions!

Where can I find more examples?

These are just a sampling of options and uses for CommonsChunkPlugin(). To find more, check out our /examples directory in our webpack/webpack core GitHub repo! If you have a great idea for more, feel free and submit a Pull Request!


No time to help contribute? Want to give back in other ways? Become a Backer or Sponsor to webpack by donating to our open collective. Open Collective not only helps support the Core Team, but also supports contributors who have spent significant time improving our organization on their free time! ❤