Webpack Optimizations - Production-ready React App
Webpack
Webpack is a static module bundler for modern JavaScript applications(Eg. React). When webpack processes our application, it internally builds a dependency graph that maps every module our project needs and generates one or more bundles.
The code/configs used in this blog are available in this repo.
A simple webpack.config.js for React application looks like this.
const path = require("path");
const HtmlWebPackPlugin = require("html-webpack-plugin");
module.exports = {
output: {
path: path.resolve(__dirname, "build"),
filename: "bundle.js",
},
resolve: {
modules: [path.join(__dirname, "src"), "node_modules"],
alias: {
react: path.join(__dirname, "node_modules", "react"),
},
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
},
{
test: /\.css$/,
use: [
{
loader: "style-loader",
},
{
loader: "css-loader",
},
],
},
{
// If you are not using less ignore this rule
test: /\.less$/,
use: [
{
loader: "style-loader",
},
{
loader: "css-loader",
},
{
loader: "less-loader",
},
],
},
],
},
plugins: [
new HtmlWebPackPlugin({
template: "./index.html",
}),
],
};
Out of the box, with this above config, the webpack generates one JS bundle file. For large projects/applications, this bundle size becomes very large(in MiBs). So it is essential to split the single bundle into multiple chunks and load them only when needed.
Here's where lazy loading in React comes in. It's basically importing the component only when needed. Lazy loading components on the route level is a good start.
When we lazy load components, webpack creates multiple bundle files based on our routes, without needing any extra configuration.
We can use hash file names for our bundles, which changes only when we build our app after doing modifications in that particular chunk. So when there is no change, the same hash will be retained and the browser serves those bundle files from the cache. Refer this for other hash options
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].[chunkhash].bundle.js',
}
It's great that we split our bundles based on routes without any additional configuration in webpack, but still, our main bundle contains all the vendor codes(node_modules). We can add a few configs to tell webpack how we want to further spit the bundles.
optimization: {
splitChunks: {
cacheGroups: {
vendors: {
test: /node_modules\/(?!antd\/).*/,
name: "vendors",
chunks: "all",
},
// This can be your own design library.
antd: {
test: /node_modules\/(antd\/).*/,
name: "antd",
chunks: "all",
},
},
},
runtimeChunk: {
name: "manifest",
},
}
Let's go through the config. optimization.splitChunks.cacheGroups
is where we define our chunks. Here I used the chunk name vendors
for all the dependencies in node_modules
except antd
(Ant Design is a UI component library) and I used the chunk name antd
for the Ant design dependency alone.
The reason why we are making the vendors separate is that, once our project gets matured, we won't be frequently adding any new dependencies, so our chunk filename hash won't be changing for every build and the browser can serve this vendor chunk from the cache. I separated antd from the vendor chunk because this can be our own design library where we frequently add/update components, so any change in this chunk should not affect our vendor chunk hash. I also extracted out the manifest which webpack maintains, containing information needed to run our application.
If you noticed the build output, our vendor chunk is highlighted in yellow and marked as [big]. Webpack is configured to warn us if the bundle size is more than 244KiB. We can safely ignore this warning because anyway our bundles should be gzipped and transferred over the network. This gzip encoding is done by default in some of the static file servers like netlify, serve and it is easy to configure in others AWS CloudFront Anyways, if we want to gzip and tell webpack to use gzipped files for calculation, we can add the below config.
const CompressionPlugin = require('compression-webpack-plugin');
plugins: [
new CompressionPlugin({
test: /\.js(\?.*)?$/i,
}),
],
performance: {
hints: "warning",
// Calculates sizes of gziped bundles.
assetFilter: function (assetFilename) {
return assetFilename.endsWith(".js.gz");
},
}
To sum up,
- We set up a minimum webpack config to run our react app.
- We used lazy loading to split our single bundle into multiple chunks.
- We used hash filenames to version our bundle files.
- We spit our main bundle further creating vendor and antd chunks.
- We used gzip to compress our bundles(also need to be done in our static hosting server) to avoid webpack bundle size warnings.
You can see the full webpack configuration here
That's it, folks, Thanks for reading this blog. Hope it's been useful for you.