- 1. Webpack 4 course – part one. Entry, output and ES6 modules
- 2. Webpack 4 course – part two. Using loaders to handle scss, image files and transpile JS
- 3. Webpack 4 course – part three. Working with plugins
- 4. Webpack 4 course – part four. Code splitting with SplitChunksPlugin
- 5. Webpack 4 course – part five. Built-in optimization for production
- 6. Webpack 4 course – part six. Increasing development experience
- 7. Webpack 4 course – part seven. Decreasing the bundle size with tree shaking
- 8. Webpack 4 course – part eight. Dynamic imports with prefetch and preload
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!
Awesome, thanks for writing about this. I’m using an older version of webpack with a react app that at some point will need to be split up, so this is helpful.
Any chance you’d consider adding a small spot at the bottom about what that looks like for actually using the files? is there a single entry file that then knows how to load the chunks? i’m not sure what this would look like.. it seems like having different files could be problematic if you’re including them yourself in the html, no?
Thanks again.
I’d advise you to use HtmlWebpackPlugin, which will include all of the files in the HTML for you and give you the output HTML file. Cheers! 🙂
I had the same problem and HtmlWebpackPlugin don’t allow you to extract easily all async chunks (vendors~a~b.[chunkhash].bundle.js) sorted by entry points to use it.
After several days of research, i’ve decided to created the plugin for my usage (ChunksWebpackPlugin).
I hope this can help you : https://www.npmjs.com/package/chunks-webpack-plugin
Thanks for the article. Very useful and simpler than going through bendless documentation pages. Looking forward for next episode of this topic
I like your articles about webpack. I’m looking forward to read about more advanced aspects and techniques.
Hello sir, I’ve come across a problem, my splitchunk bundle file is created but I’m not able to access it, it shows in my command prompt that the common files from node modules have been bundled in this file, but when I run my project it just doesn’t work, I mean I get blank screen with just background and non node_modules files,
I want to ask you whether there’s any script tag or anything that I need to add, so that browser considers the splitchunk bundled file
This article helped me a lot while learning new thins. Thanks for the information!