nikhil image

Nikhil Kumaran S

Webpack Optimizations - Production-ready React App

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",
    }),
  ],
};

single bundle file

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.

multiple bundles

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',
}

multiple bundles with hash

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",
  },
}

multiple bundles with hash and with further vendor chunk

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");
  },
}

multiple bundles with hash and with further vendor chunk and gzipped

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.

References: