Here’s a guide to the 8 most useful optimizations for optimizing Webpack size and build speed.
This article will be updated when a permanently stable version of Webpack 5 is released (version 5.6 is not yet fully complete).
For information, you can find the complete configuration that I use in this article #Configuration de Webpack.
The following options are sorted in order of efficiency which allowed me to reduce the size of the generated bundle by 80% and the build time by a factor of 5!
Measuring tools
First, to understand the effectiveness of these options, here are two tools to measure the impact of changes.
Calculate the size of the bundles
The package webpack-bundle-analyzer
allows you to see the detailed content of each bundle generated as well as their total size. At a glance, it is possible to identify redundant packages between bundles.
Start by adding the package to your sources:
yarn add -D webpack-bundle-analyzer
Then add it to the configuration of your project (it will then be launched automatically for each build in a dedicated window using port 8888):
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
Then in the list of plugins, add it with this configuration:
new BundleAnalyzerPlugin({
/* Can be `server`, `static` or `disabled`. */
/* In `server` mode analyzer will start HTTP server to show bundle report. */
/* In `static` mode single HTML file with bundle report will be generated. */
/* In `disabled` mode you can use this plugin to just generate Webpack Stats JSON file by setting `generateStatsFile` to `true`. */
analyzerMode: 'server',
/* Host that will be used in `server` mode to start HTTP server. */
analyzerHost: '127.0.0.1',
/* Port that will be used in `server` mode to start HTTP server. */
analyzerPort: 8888,
/* Path to bundle report file that will be generated in `static` mode. */
/* Relative to bundles output directory. */
reportFilename: 'report.html',
/* Module sizes to show in report by default. */
/* Should be one of `stat`, `parsed` or `gzip`. */
/* See "Definitions" section for more information. */
defaultSizes: 'parsed',
/* Automatically open report in default browser */
openAnalyzer: true,
/* If `true`, Webpack Stats JSON file will be generated in bundles output directory */
generateStatsFile: false,
/* Name of Webpack Stats JSON file that will be generated if `generateStatsFile` is `true`. */
/* Relative to bundles output directory. */
statsFilename: 'stats.json',
/* Options for `stats.toJson()` method. */
/* For example you can exclude sources of your modules from stats file with `source: false` option.
/* See more options here: https://github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21 */
statsOptions: null,
/* Log level. Can be 'info', 'warn', 'error' or 'silent'. */
logLevel: 'info'
})
Generation time
The simplest tools are often the most effective.
On Linux, launch the build by prefixing the command with:
time npm run build
The total time will be displayed at the end of the execution of the order.
1. Tree Shaking
Since version 4, Wepack offers to automatically remove dependencies not used in the project.
To activate it, add the following line in optimization
the configuration part of Webpack:
sideEffects: true
It will then delete all the packages that are called directly but also all the indirect dependencies like polyfills or external libs!
To solve this problem, add the files to ignore in package.json
, for example:
"sideEffects": [
"*.scss",
"*.css",
"*polyfills.js",
"*application.js",
"*other-libs*"
],
Typically, it is necessary to include in this list the libraries which call jQuery without an import
or require
. Likewise, be sure to include css type files when using CSS-in-JS.
2. Automatic chunks
Webpack proposes to automatically create “chunks” to pool the imports present in the various files declared in entry
.
In the configuration of Webpack, declare the chunks to be shared. For example :
runtimeChunk: {
name: 'runtime'
},
splitChunks: {
chunks: 'async',
minSize: 30000,
maxSize: 0,
minChunks: 2,
maxAsyncRequests: 7,
maxInitialRequests: 4,
name: true,
cacheGroups: {
default: false,
commonsAdmins: {
name: 'admins-commons',
minChunks: 2,
reuseExistingChunk: true,
chunks: function (chunk) {
return chunk.name.includes('admin');
}
},
commons: {
name: 'commons',
minChunks: 2,
reuseExistingChunk: true,
chunks: function (chunk) {
return !module.resource.includes('/admin/');
}
}
}
},
In this configuration, two modules are created by default: a common one commons
and one for the components used by the admin part admin-commons
(often contain unwanted dependencies for simple visitors or users).
Additionally some useful properties are specified:
- minSize: the component must be at least 30kb to be generated
- minChunks: minimum number of chunks that must contain a dependency to be pooled
- maxAsyncRequests: maximum number of async requests to load components
- maxInitialRequests: maximum number of requests at initial load
All that remains is to include the chunks in common in the code:
<script src="/runtime.js"></script>
<script src="/commons.js"></script>
<script src="/user.js"></script>
3. Load the components asynchronously
Since ES2015 and Babel 7, dynamic imports are managed natively. So, it is very easy to call JS files dynamically and on demand. In addition, in the case of React, it provides a utility,, React.lazy
to manage it even more easily.
Create the following function in your sources:
export const lazyWithPreload = (factory) => {
const Component = React.lazy(factory);
Component.preload = factory;
return Component;
};
Then declare all the components you want to call dynamically as follows:
export const CommentBox = lazyWithPreload(() => import(/* webpackChunkName: "comment-box */ '../comments/box'));
While thinking about naming the import with the comment “magic”: webpackChunkName
.
Then import your component, like a normal component:
import CommentBox from '../loaders/commentBox';
In the code, then call the component CommentBox
as usual:
{
this.state.isCommentDisplay &&
<CommentBox props={...}/>
}
Webpack will then take care of loading all the components asynchronously during the actual call in the code and only then.
4. Change the file generation mode
By default, Webpack does not generate SourceMaps (essential files to find the origin of errors). Several options are available: webpack.js.org
The most efficient option in terms of generation time and available source is:
webPackConfig.devtool = 'cheap-module-source-map';
5. Caching builds
Fastest option to implement, enable cache to optimize the following builds:
webPackConfig.cache = true;
6. Limit the number of files in entries
Try not to create new files for each new page on your site. The greater the number of files, the longer the build time will be. By mutualizing the files, I reduced the incremental build time by more than 5. A single modification took more than 10 seconds, going to 5 base files, I am only 2 seconds.
One solution is to create a generic file which will be in charge of calling the dynamic component to load. This file can be used for example only to display the header and the footer, all the content will then be loaded dynamically.
7. Limit the total number of files to parse
In the options of rules
, provide Webpack only with local components (those of external libraries are already compiled normally, no need to do the job twice):
rules: [
{
test: /\.(js|jsx)$/,
include: path.resolve('./assets/javascripts'),
options: {
babelrc: true,
cacheDirectory: true
}
},
{
test: /\.s?[ac]ss$/,
exclude: /node_modules/,
use: ...
},
{
test: /\.(gif|png|jpe?g|svg)$/i,
exclude: /node_modules/,
use: ...
},
{
test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/i,
exclude: /node_modules/,
use: ...
}
]
8. Decrease the size of big libs
Libraries like lodash are often useful but can load several hundred kb. It is then necessary to use a mechanism to convert the imports so that Webpack can optimize them.
Add the package to your project:
yarn add babel-plugin-lodash
Then in .babelrc
:
"plugins" : [
"lodash" ,
...
]
Finally in the configuration of your project:
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
webPackConfig.plugins = [
new LodashModuleReplacementPlugin({
'caching': true,
'collections': true,
'flattening': true,
'placeholders': true
})
];
Otherwise replace the use of Lodash by native methods in JS:
https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore
Unfortunately, no equivalent package is provided for jQuery. You must import all the lib … The only option is to use the slim package provided by jQuery. Then declare an alias in your configuration so that all dependencies use it:
alias: {
react: 'node_modules/react',
'react-dom': 'node_modules/react-dom',
jquery: 'node_modules/jquery/dist/jquery.slim'
},