An in-depth guide to performance optimization with webpack
Tags: webpack, webperf
Published: 2020-05-26 13:25:25

---
title: An in-depth guide to performance optimization with webpack
published: true
date: 2020-05-07 13:00:00 UTC
tags: webpack,webperf
canonical_url: https://blog.logrocket.com/guide-performance-optimization-webpack/
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/xa5o89j7zqm6l8nr33v8.jpeg
---
**Written by [Sebastian Weber](https://blog.logrocket.com/author/sebastianweber/)**✏️

These days, you have to use a module bundler like [webpack](https://webpack.js.org/) to benefit from a development workflow that utilizes state-of-the-art performance optimization concepts. Module bundlers are built by brilliant people just to help you with these difficult tasks.

In addition, I recommend using a [starter kit](https://webpack.js.org/starter-kits/) or a modern boilerplate project with webpack configuration best practices already in place. Building on this, you can make project-specific adjustments. I like [this React + webpack 4 boilerplate project](https://github.com/esausilva/react-starter-boilerplate-hmr), which is also the basis of this blog’s accompanying demo project.

webpack 4 comes with appropriate presets. However, you have to understand a fair number of concepts to reap their performance benefits. Furthermore, the possibilities to tweak webpack’s configuration are endless, and you need extensive knowhow to do it the right way for your project.

You can follow along the examples with [my demo project](https://github.com/doppelmutzi/react-performance-strategies). Just switch to the mentioned branch so you can see for yourself the effects of the adjustments. The `master` branch represents the demo app, along with its initial webpack configuration. Just execute `npm run dev` to take a look at the project (the app opens on [`localhost:3000`](localhost:3000).

```jsx
// package.json
"scripts": {
  "dev": "webpack-dev-server --env.env=dev",
  "build": "webpack --env.env=prod"
}
```

## Use production mode for built-in optimization

webpack 4 has introduced [development and production modes](https://webpack.js.org/configuration/mode/). You should always ship a production build to your users. I want to emphasize this because you get a number of built-in optimizations automatically, such as [tree shaking](https://webpack.js.org/guides/tree-shaking/), [performance hints](https://webpack.js.org/configuration/performance/#performancehints), or minification with the [`TerserWebpackPlugin`](https://webpack.js.org/plugins/terser-webpack-plugin/) in production mode.

webpack 4 has adopted the software engineering paradigm [convention over configuration](https://en.wikipedia.org/wiki/Convention_over_configuration), which basically means that you get a meaningful configuration simply by setting the mode to `"production"`. However, you can tweak the configuration by overriding the default values. Throughout this article, we explain some of the performance-related configuration options that you can define as you wish for your project.

If you run `npm run build` (on the [`master`](https://github.com/doppelmutzi/react-performance-strategies) branch), you get the following output:

![Build Output For Master Branch](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/build-output-master.png?resize=730%2C147&ssl=1)

Open `dist/static/app.js` and you can see for yourself that webpack has uglified our bundle.

If you check out the branch [`use-production-mode`](https://github.com/doppelmutzi/react-performance-strategies/tree/use-production-mode), you can see how the `app` bundle size increases when you set the mode to `"development"` because some optimizations are not performed.

```jsx
// webpack.prod.js
const config = {
+  mode: "development",
-  mode: "production",
  // ...
}
```

The result looks like this:

![Build Output In Development Mode](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/development-mode-output-master.png?resize=730%2C152&ssl=1)

## Use webpack-bundle-analyzer regularly

You should use the awesome [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) plugin regularly to understand what your bundles consist of. In so doing, you realize what components really reside inside your bundles and find out which components and dependencies make up the bulk of their size — and you may discover modules that got there by mistake.

The following two npm scripts run a webpack build (development or production) and open up the bundle analyzer:

```jsx
// package.json
"scripts": {
  "dev:analyze": "npm run dev -- --env.addons=bundleanalyzer",
  "build:analyze": "npm run build -- --env.addons=bundleanalyzer"
}
```

When you invoke `npm run build:analyze`, an interactive view opens on [`http://localhost:8888`](http://localhost:8888/).

![Bundle Analyzer Interactive View In The Browser](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/bundle-analyzer-interactive-view.png?resize=730%2C342&ssl=1)

Our initial version of the demo project consists of only one bundle named `app.js` because we only defined a single [entry point](https://webpack.js.org/concepts/entry-points/).

```jsx
// webpack.prod.js
const config = {
  mode: "production",
  entry: {
    app: [`${commonPaths.appEntry}/index.js`]
  },
  output: {
    filename: "static/[name].js",
  },
  // ...
}
```

## React production build

You should send the [minified React production build](https://reactjs.org/docs/optimizing-performance.html#use-the-production-build) to your users. Bundled React code is much smaller because it doesn’t include things that are really only useful during development time, like warnings.

If you are using webpack in production mode, you come up with a React production build, as you can see in the last screenshot (`react-dom.production.min.js`).

The following screenshot shows a little bit more about the bundled React code.

![Interactive View Of The Bundled React Code In Production Mode](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/bundled-react-code-interactive-view.png?resize=730%2C777&ssl=1)

If you take a closer look, you will see that there is still some code that does not belong in a production build (e.g., react-hot-loader). This is a good example of why frequent analysis of our generated bundles is an important part of finding opportunities for further optimization. In this case, the webpack/React setup [needs improvement to exclude react-hot-loader](https://github.com/gaearon/react-hot-loader/issues/602#issuecomment-575741927).

If you have an “insufficient production config” and set `mode: "development"`, then you come up with a much larger React chunk (switch to branch `use-production-mode` and execute `npm run build:analyze`).

![Interactive View Of The Bundled React Code In Development Mode](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/bundled-react-code-interactive-view-development-mode.png?resize=730%2C775&ssl=1)

## Add multiple entry points for bundle splitting

Instead of sending all our code in one large bundle to our users, our goal as frontend developers should be to serve as little code as possible. This is where code splitting comes into play.

Imagine that our user navigates directly to the profile page via the route `/profile`. We should serve only the code for this profile component. Here, we’ll add another entry point that leads to a new bundle (check out the [`entry-point-splitting`](https://github.com/doppelmutzi/react-performance-strategies/tree/entry-point-splitting) branch).

```jsx
// webpack.prod.js
const config = {
  mode: "production",
  entry: {
    app: [`${commonPaths.appEntry}/index.js`],
+   profile: [`${commonPaths.appEntry}/profile/Profile.js`],
  },
  // ...
}
```

The consequence, of course, is that you have to make sure the bundles are included in one or more HTML pages depending on the project, whether a single-page application (SPA) or multi-page application (MPA). This demo project constitutes an SPA.

The good thing is that you can automate this composing step with the help of [`HtmlWebpackPlugin`](https://github.com/jantimon/html-webpack-plugin).

```jsx
// webpack.prod.js
plugins: [
  // ...
  new HtmlWebpackPlugin({
    template: `public/index.html`,
    favicon: `public/favicon.ico`,
  }),
],
```

Run the build, and the generated `index.html` consists of the correct link and script tags in the right order.

```jsx
<!-- dist/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>react performance demo</title>
    <link rel="shortcut icon" href="favicon.ico" />
    <link href="styles/app.css" rel="stylesheet" />
    <link href="styles/profile.css" rel="stylesheet" />
  </head>
  <body>
    <div id="root"></div>
    <script src="static/app.js"></script>
    <script src="static/profile.js"></script>
  </body>
</html>
```

To demonstrate how to generate multiple HTML files (for an MPA use case), check out the branch [`entry-point-splitting-multiple-html`](https://github.com/doppelmutzi/react-performance-strategies/tree/entry-point-splitting-multiple-html). The following setup generates an `index.html` and a `profile.html`.

```jsx
// webpack.prod.js
plugins: [
-  new HtmlWebpackPlugin({
-    template: `public/index.html`,
-    favicon: `public/favicon.ico`,
-  }),
+  new HtmlWebpackPlugin({
+    template: `public/index.html`,
+    favicon: `public/favicon.ico`,
+    chunks: ["profile"],
+    filename: `${commonPaths.outputPath}/profile.html`,
+  }),
+  new HtmlWebpackPlugin({
+    template: `public/index.html`,
+    favicon: `public/favicon.ico`,
+    chunks: ["app"],
+  }),
],
```

There are even more [configuration options](https://github.com/jantimon/html-webpack-plugin#options). For example, you can provide a custom chunk-sorting function with `chunksSortMode`, as demonstrated [here](https://medium.com/@jaketripp/cool-things-with-webpack-9a8019bdbd4a).

webpack only includes the script for the profile bundle in the generated `dist/profile.html` file, along with a link to the corresponding `profile.css`. `index.html` looks similar and only includes the app bundle and `app.css`.

```jsx
<!-- dist/profile.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>react performance demo</title>
    <link rel="shortcut icon" href="favicon.ico" />
    <link href="styles/profile.css" rel="stylesheet" />
  </head>
  <body>
    <div id="root"></div>
    <script src="static/profile.js"></script>
  </body>
</html>
```

## Separate application code and third-party libs

Check out the [`vendor-splitting`](https://github.com/doppelmutzi/react-performance-strategies/tree/vendor-splitting) branch. Let’s analyze the current state of our production build.

![Analyzing Third-party Libraries In The Production Build Of The vendor-splitting Branch](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/vendor-splitting-interactive-view.png?resize=730%2C370&ssl=1)

`react.production.min.js` and `lodash.js` are found redundantly in both bundles.

Is this a problem? Yes, because for any kind of app, you should separate the vendor chunks from your application chunks due to caching purposes. Your application code simply changes more often than the vendor code because you adjust versions of your dependencies less frequently.

Vendor bundles can thus be cached for a longer time, which benefits returning users. Vendor splitting means creating separate bundles for your application code and third-party libraries.

You can do this — and other code splitting techniques, as we will see in a minute — with the [`SplitChunksPlugin`](https://webpack.js.org/plugins/split-chunks-plugin/). Before we add vendor splitting, let’s take a look at the current production build output first.

![Production Build Output Of The vendor-splitting Branch](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/production-build-output-vendor-splitting-e1588690991281.png?resize=730%2C173&ssl=1)

In order to use the aforementioned `SplitChunksPlugin`, we add [`optimization.splitChunks`](https://webpack.js.org/plugins/split-chunks-plugin/#optimizationsplitchunks) to our config.

```jsx
// webpack.prod.js
const config = {
  // ...
+ optimization: {
+   splitChunks: {
+     chunks: "all",
+   },
+ },
}
```

This time, we run the build with the bundle analyzer option (`npm run build:analyze`). The result of the build looks like this:

![Build Output Using The SplitChunksPlugin](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/build-output-splitchunksplugin.png?resize=730%2C182&ssl=1)

`app.js` and `profile.js` are much smaller now. But wait — two new bundles, `vendors~app.js` and `vendors~app~profile.js`, have come up. As you can see, with the bundle analyzer, we successfully extracted dependencies from `node_modules` (i.e., React, Lodash) into separate bundles. The tiny greenish boxes in the lower right are our `app` and `profile` bundles.

![Bundle Analyzer Interactive View Of The Build Output With SplitChunksPlugin](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/bundle-analyzer-splitchunksplugin.png?resize=730%2C389&ssl=1)

The names of the vendor bundles indicate from which application chunk the dependencies were pulled. Since React DOM is only used in the `index.js` file of the `app.js` bundle, it makes sense that the dependency is only inside of `vendors~app.js`. But why is Lodash in the `vendors~app~profile.js` bundle if it’s only used by the `profile.js` bundle?

Once again, this is webpack magic: [conventions/default values](https://webpack.js.org/plugins/split-chunks-plugin/#optimizationsplitchunks), plus some clever things under the hood. I really recommend reading [this article](https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366) by webpack core member Tobias Koppers on the topic. The important takeaways are:

- Code splitting is based on heuristics that find candidates for splitting based on module duplication count and module category
- The default behavior is that only dependencies of ≥30KB are picked as candidates for the vendor bundle
- Sometimes webpack intentionally duplicates code in bundles to minimize requests for additional bundles

But the default behavior can be changed by the [`splitChunks`](https://webpack.js.org/plugins/split-chunks-plugin/#optimizationsplitchunks) options. We can set the [`minSize` option](https://webpack.js.org/plugins/split-chunks-plugin/#splitchunksminsize) to a value of 600KB and thereby tell webpack to first create a new vendor bundle if the dependencies it pulled out exceed this value (check out out the [`vendor-splitting-tweaking`](https://github.com/doppelmutzi/react-performance-strategies/tree/vendor-splitting-tweaking) branch).

```jsx
// webpack.prod.js
const config = {
  // ...
  optimization: {
   splitChunks: {
     chunks: "all",
+    minSize: 1000 * 600
   },
 },
}
```

Now, webpack came up with only one vendor bundle, named `vendors~app.js`.

![Bundle Analyzer Interactive View Of The Vendor Bundle](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/bundle-analyzer-vendor-bundle.png?resize=730%2C645&ssl=1)

Why? If you check the `Stat` size, at 750KB, the new vendor bundle is larger than the minimum size of 600KB. webpack couldn’t create another chunk candidate for another vendor bundle because of our new configuration.

As you can see, Lodash is duplicated in the `profile.js` bundle, too (remember, webpack intentionally duplicates code). If you adjust the value of `minSize` to, e.g., 800KB, webpack cannot come up with a single vendor bundle for our demo project. I leave it up to you to try this out.

Of course, you could have more control if you wanted. Let’s assign a custom name: `node_vendors`. We define a [`cacheGroups`](https://webpack.js.org/plugins/split-chunks-plugin/#splitchunkscachegroups) property for our vendors, which we pull out of the `node_modules` folder with the `test` property (check out the [`vendor-splitting-cache-groups`](https://github.com/doppelmutzi/react-performance-strategies/tree/vendor-splitting-cache-groups) branch). In the previous example, the default values of `splitChunks` contains `cacheGroups` out of the box.

```jsx
// webpack.prod.js
const config = {
  // ...
- optimization: {
-   splitChunks: {
-     chunks: "all",
-    minSize: 1000 * 600
-   },
- },
+ optimization: {
+   splitChunks: {
+     cacheGroups: {
+      vendor: {
+       name: "node_vendors", // part of the bundle name and
+         // can be used in chunks array of HtmlWebpackPlugin
+         test: /[\\/]node_modules[\\/]/,
+         chunks: "all",
+       },
+     },
+   },
+ },
  // ...
}
```

This time, webpack presents us with one vendor bundle with our custom name and all dependencies from `node_modules` combined in it.

![Bundle Analyzer View Of The Vendor Bundle With node_modules](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/bundle-analyzer-vendor-bundle-node-modules.png?resize=730%2C816&ssl=1)

In the `splitChunks` property above, we added a `chunks` property with a value of `all`. Most of the time, this value should be alright for your project. However, if you want to extract only lazy- or async-loaded bundles, you can use the value `async`. When we look at common code splitting in the next section, we’ll revisit this aspect.

[![LogRocket Free Trial Banner](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/f760c-1gpjapknnuyhu8esa3z0jga.png?resize=1200%2C280&ssl=1)](https://logrocket.com/signup/)

## Consolidate shared code into common bundles

Common code splitting aims to combine shared code into separate bundles to achieve smaller bundle sizes and more efficient caching. It’s similar to vendor code splitting, but the difference is that you target shared components and libraries within your own codebase.

In our example, we combine vendor code splitting with common code splitting (check out the branch [`common-splitting`](https://github.com/doppelmutzi/react-performance-strategies/tree/common-splitting)). We import and use the `add` function of `util.js` in the `Blog` and `Profile` components, which are part of two different entry points.

We add a new object with the key `common` to the `cacheGroups` object, with a regex to target only modules in our `components` folder. Since the components of this demo project are very tiny, we need to override webpack’s default value of `minSize`. We’ll set it to 0KB just in case.

```jsx
// webpack.prod.js
const config = {
  // ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          name: "node_vendors", // part of the bundle name and
          // can be used in chunks array of HtmlWebpackPlugin
          test: /[\\/]node_modules[\\/]/,
          chunks: "all",
        },
+       common: {
+         test: /[\\/]src[\\/]components[\\/]/,
+         chunks: "all",
+         minSize: 0,
+       },
      },
    },
  },
```

Running webpack generates a new bundle, `common~app~profile.js`, in addition to the two entry point bundles and our vendor bundle.

![Build Output Of The common-splitting Branch](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/build-output-common-splitting.png?resize=730%2C257&ssl=1)

Search for `console.log("add")` within `dist/static/common~app~profile.js` and you’ll find what you’re looking for.

## Lazy loading on the route level

We’ve already seen how we can use [react-router](https://reacttraining.com/react-router/web) in combination with entry code splitting in the `entry-point-splitting` branch. We can push code splitting one step further by enabling our users to load different components on demand while they are navigating through our application. This use case is possible with [dynamic imports](https://webpack.js.org/guides/code-splitting/#dynamic-imports) and code splitting by route.

In the context of our demo project, this is evidenced by the fact that the `profile.js` bundle is only loaded when the user navigates to the corresponding route (switch to branch [`code-splitting-routes`](https://github.com/doppelmutzi/react-performance-strategies/tree/code-splitting-routes)). We utilize the [@loadable/components](https://github.com/gregberge/loadable-components) library, which does the hard work of code splitting and dynamic loading behind the scenes.

```jsx
// package.json
{
  // ...
  "dependencies": {
    "@loadable/component": "^5.12.0",
    // ...
  }
  // ...
}
```

In order to use the [ES6 dynamic import syntax](https://github.com/tc39/proposal-dynamic-import), we have to use a [babel plugin](https://babeljs.io/docs/en/babel-plugin-syntax-dynamic-import).

```jsx
// .babelrc
{
  // ...
  "plugins": [
    "@babel/plugin-syntax-dynamic-import",
    // ...
  ]
}
```

The implementation is pretty straightforward. First, we need a tiny wrapper around our `Profile` component that we want to lazy-load.

```jsx
// ProfileLazy.js
import loadable from "@loadable/component";
export default loadable(() =>
  import(/* webpackChunkName: "profile" */ "./Profile")
);
```

We use the dynamic import inside the `loadable` callback and add a comment (to give our code-split chunk a custom name) that is understood by webpack.

In our `Blog` component, we just have to adjust the import for our `Router` component.

```jsx
// Blog.js
import React from "react";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
- import Profile from "../profile/Profile";
+ import Profile from "../profile/ProfileLazy";
import "./Blog.css";
import Headline from "../components/Headline";

export default function Blog() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/">Blog</Link>
          </li>
          <li>
            <Link to="/profile">Profile</Link>
          </li>
        </ul>
        <hr />
        <Switch>
          <Route exact path="/">
            <Articles />
          </Route>
          <Route path="/profile">
            <Profile />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

// ...
```

The last step is to delete the `profile` entry point that we used for entry point code splitting from both the production and development webpack config.

```jsx
// webpack.prod.js and webpack.dev.js
const config = {
  // ...
  entry: {
    app: [`${commonPaths.appEntry}/index.js`],
-  profile: [`${commonPaths.appEntry}/profile/Profile.js`],
  },
  // ...
}
```

Run a production build and you’ll see in the output that webpack created a `profile.js` bundle because of the dynamic import.

![Production Build Output For The code-splitting-routes Branch](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/production-build-output-code-splitting-routes.png?resize=730%2C185&ssl=1)

The generated HTML document does not contain a script pointing to the `profile.js` bundle.

```jsx
// diest/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>react performance demo</title>
    <link rel="shortcut icon" href="favicon.ico" />
    <link href="styles/app.css" rel="stylesheet" />
  </head>
  <body>
    <div id="root"></div>
    <script src="static/node_vendors.js"></script>
    <script src="static/app.js"></script>
  </body>
</html>
```

Wonderful — `HtmlWebpackPlugin` did the right job.

It’s easier to test this with the `development` build (`npm run dev`).

![Examining The Development Build For The Homepage In DevTools](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/devtools-development-build-homepage.png?resize=730%2C797&ssl=1)

DevTools shows that the `profile.js` bundle is not loaded initially. When you navigate to the `/profile` route, though, the bundle gets loaded lazily.

![Examining The Development Build For The Profile Route In DevTools](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/devtools-development-build-profile-route.png?resize=730%2C725&ssl=1)

## Lazy loading on the component level

Code splitting is also possible on a fine-grained level. Check out branch [`code-splitting-component-level`](https://github.com/doppelmutzi/react-performance-strategies/tree/code-splitting-component-level) and you’ll see that the technique of route-based lazy loading can be used to load smaller components based on events.

In the demo project, we just add a button on the profile page to load and render a simple React component, `Paragraph`, on click.

```jsx
// Profile.js
- import React from "react";
+ import React, { useState } from "react";
// some more boring imports
+ import LazyParagraph from "./LazyParagraph";
const Profile = () => {
+ const [showOnDemand, setShowOnDemand] = useState(false);
  const array = [1, 2, 3];
  _.fill(array, "a");
  console.log(array);
  console.log("add", add(2, 2));
  return (
    <div className="profile">
      <Headline>Profile</Headline>
      <p>Lorem Ipsum</p>
+      {showOnDemand && <LazyParagraph />}
+      <button onClick={() => setShowOnDemand(true)}>click me</button>
    </div>
  );
};
export default Profile;
```

`ProfileLazy.js` looks familiar. We want to call the new bundle `paragraph.js`.

```jsx
// LazyParagraph.js
import loadable from "@loadable/component";
export default loadable(() =>
  import(/* webpackChunkName: "paragraph" */ "./Paragraph")
);
```

I skip `Paragraph` because it is just a simple React component to render something on the screen.

The output of a production build looks like this.

![Production Build Output Of Component Level Splitting](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/production-build-output-component-level-splitting.png?resize=730%2C207&ssl=1)

The development build shows that loading and rendering the dummy component bundled into `paragraph.js` happens only on clicking the button.

![Examining The Component Level Development Build In DevTools](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/paragraph-component-development-build-devtools.png?resize=706%2C868&ssl=1)

## Extracting webpack’s manifest into a separate bundle

What does this even mean? Every application or site built with webpack includes a [runtime and manifest](https://webpack.js.org/concepts/manifest/). It’s the boilerplate code that does the magic. The manifest wires together our code and the vendor code. It is not especially large, but it is duplicated for every entry point unless you do something about it.

Once again, if you want to optimize browser caching, then it might be useful to extract the manifest into a separate bundle. This saves your users from unnecessarily re-downloading files. However, I’m not sure how often the manifest changes — after webpack version updates?

Anyway, check out the branch [`manifest-splitting`](https://github.com/doppelmutzi/react-performance-strategies/tree/manifest-splitting). The basis is our production configuration with two entry points and, now, vendor/common code splitting in place. In order to extract the manifest, we have to add the [`runtimeChunk`](https://webpack.js.org/guides/caching/#extracting-boilerplate) property.

```jsx
// webpack.prod.js
const config = {
  mode: "production",
  entry: {
    app: [`${commonPaths.appEntry}/index.js`],
    profile: [`${commonPaths.appEntry}/profile/Profile.js`],
  },
  output: {
    filename: "static/[name].js",
  },
+  optimization: {
+    runtimeChunk: {
+      name: "manifest",
+    },    
+  },
  // ...
}
```

A production build shows a new `manifest.js` bundle. The two entry point bundles are a little bit smaller.

![Production Build With The RuntimeChunk Optimization](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/production-build-runtimechunk.png?resize=730%2C215&ssl=1)

In contrast, here you can see the bundle sizes without the `runtimeChunk` optimization.

![Production Build Without The RuntimeChunk Optimization](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/production-build-without-runtimechunk.png?resize=730%2C236&ssl=1)

The two HTML documents were generated with an extra script tag for the manifest. As an example, here you can see the `profile.html` file (I skip `index.html`).

```jsx
<!-- dist/profile.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>react performance demo</title>
    <link rel="shortcut icon" href="favicon.ico" />
    <link href="styles/profile.css" rel="stylesheet" />
  </head>
  <body>
    <div id="root"></div>
    <script src="static/manifest.js"></script>
    <script src="static/profile.js"></script>
  </body>
</html>
```

## Exclude dependencies from bundles

With webpack’s [`externals`](https://webpack.js.org/configuration/externals/) config option, it is possible to exclude dependencies from bundling. This is useful if the environment provides the dependencies elsewhere.

As an example, if you work with different teams on a React-based MPA, you can provide React in a dedicated bundle that is included in the markup in front of each team’s bundles. This allows each team to keep this dependency out of its bundles.

This is the starting point (once again in branch [`manifest-splitting`](https://github.com/doppelmutzi/react-performance-strategies/tree/manifest-splitting)). We have two entry point bundles, both with React and one with the even bigger react-dom as dependencies.

![Bundle Analyzer View Of Our Entry Point With React And ReactDOM](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/bundle-analyzer-entry-point-bundle.png?resize=730%2C783&ssl=1)

![The manifest-splitting Branch Development Build Output](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/manifest-splitting-branch-bundle.png?resize=730%2C210&ssl=1)

If we want to exclude them from the production bundles, we have to add the following `externals` object to our production configuration (check out branch `externals`).

```jsx
// webpack.prod.js
const config = {
  // ...  
+  externals: {
+    react: "React",
+    "react-dom": "ReactDOM",
+  },
  // ...
}
```

When you create a production bundle, the file size is much smaller.

![Production Build Output Of The manifest-splitting Branch](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/manifest-splitting-branch-production-build-output.png?resize=730%2C365&ssl=1)

The size of `app.js` has shrunk from 223KB to 96.4KB because react-dom was left out. The file size of `profile.js` has decreased from 78.9KB to 71.7KB.

Now we have to provide React and react-dom in the context. In our example, [we provide React over CDN](https://reactjs.org/docs/cdn-links.html) by adding script tags to our HTML template files.

```jsx
<!-- public/index.html -->
<body>
  <div id="root"></div>
  <script crossorigin 
    src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
  <script crossorigin 
    src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
</body>
```

This is how the generated `index.html` looks (I’ll skip the `profile.html` file).

```jsx
<!-- dist/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>react performance demo</title>
    <link rel="shortcut icon" href="favicon.ico" />
    <link href="styles/app.css" rel="stylesheet" />
  </head>
  <body>
    <div id="root"></div>
    <script
      crossorigin
      src="https://unpkg.com/react@16/umd/react.production.min.js"
    ></script>
    <script
      crossorigin
      src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"
    ></script>
    <script src="static/manifest.js"></script>
    <script src="static/app.js"></script>
  </body>
</html>
```

## Remove dead code from bundles via tree shaking

Imagine you have a component in your codebase with default and/or named exports that isn’t used anywhere in your application. It is a total waste to deliver this code in the JavaScript bundle to your users. [Tree shaking](https://webpack.js.org/guides/tree-shaking/) is a dead code elimination technique that finds and strips that unused code from your bundles.

The good thing with webpack 4 is that you most likely don’t have to do anything to perform tree shaking. Unless you are working on a legacy project with the CommonJS module system, you get it automatically in webpack when you use production mode (you have to use ES6 module syntax, though).

Since you can use webpack without specifying a mode, or set it to `"development"`, you have to meet a set of additional requirements, which is explained in great detail [here](https://webpack.js.org/guides/tree-shaking/#conclusion). Also, as described in webpack’s tree shaking documentation, you have to make sure no compiler transforms the ES6 module syntax into the CommonJS version.

Since we use [@babel/preset-env](https://babeljs.io/docs/en/babel-preset-env#modules) in our demo project, we better set `modules: false` to be on the safe side to disable transformation of ES6 module syntax to another module type.

```jsx
// .babelrc
{
    "presets": [
      ["@babel/preset-env", { "modules": false }],
      "@babel/preset-react"
    ],
    "plugins": [
      // ...
    ]
  }
```

This is because ES6 modules can be statically analyzed by webpack, which isn’t possible with other variants, such as `require` by CommonJS. It is possible to dynamically change exports and do all kinds of [monkey patching](https://stackoverflow.com/a/42883538). You can also see an example in this project where `require` statements are created dynamically within a loop:

```jsx
// webpack.config.js

// ...
const addons = (/* string | string[] */ addonsArg) => {
  // ...
  return addons.map((addonName) =>
    require(`./build-utils/addons/webpack.${addonName}.js`)
  );
};
// ...
```

If you want to see how it looks without tree shaking, you can disable the [`UglifyjsWebpackPlugin`](https://webpack.js.org/plugins/uglifyjs-webpack-plugin/), which is used by default in production mode.

Before we disable tree shaking, I first want to demonstrate that the production build indeed removes unused code. Switch to the `master` branch and take a look at the following file:

```jsx
// util.js
export function add(a, b) { // imported and used in Profile.js
  console.log("add");
  return a + b;
}
export function subtract(a, b) { // nowhere used
  console.log("subtract");
  return a - b;
}
```

Only the first function is used within `Profile.js`. Execute `npm run build` again and open up `dist/static/app.js`. You can search and find two occurrences of the string `console.log("add")` but no occurrence of `console.log("subtract")`. This is the proof that it works.

Now switch to branch `disable-uglify`, where `minimize: false` is added to the configuration.

```jsx
// webpack.prod.js
const config = {
  mode: "production",  
+ optimization: {
+   minimize: false, // disable uglify + tree shaking
+ },
  entry: {
    app: [`${commonPaths.appEntry}/index.js`],
  },
  // ...
}
```

If you execute another production build, you get a warning that your bundle size exceeds the standard threshold.

![Production Build Output With Warnings](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/build-output-warnings.png?resize=730%2C606&ssl=1)

In the non-defaced `dist/static/app.js` file, you also find two occurrences of `console.log("subtract")`.

But why is the uglify plugin even involved? Well, the term tree shaking is a bit [misleading](https://medium.com/@Rich_Harris/tree-shaking-versus-dead-code-elimination-d3765df85c80): in this step, the dead leaves (e.g., unused functions) of the dependency graph are not removed. Instead, the uglify plugin does this in the second step. In the first step, a live code inclusion takes place to mark the source code in a way that the uglify plugin is able to “shake the syntax tree.”

## Define a performance budget

It’s crucial to monitor the bundle sizes of your project. As the application grows over time, it’s important to understand when bundles grow too large and start impacting UX.

As you can see in the last screenshot, webpack has built-in support for [performance measurement](https://webpack.js.org/configuration/performance/) of assets. In the example above, we have colossally blown up the maximum threshold of 244KB with 747KB. In production mode, the default behavior is to warn you about threshold violations.

We can define our own [performance budget](https://addyosmani.com/blog/performance-budgets/) for output assets. We don’t care for `development`, but we want to keep an eye on it for production (check out branch [`performance-budget`](https://github.com/doppelmutzi/react-performance-strategies/tree/performance-budget)).

```jsx
// webpack.prod.js
const config = {
  mode: "production",
  entry: {
    app: [`${commonPaths.appEntry}/index.js`],
  },
  output: {
    filename: "static/[name].js",
  },
+ performance: {
+   hints: "error",
+   maxAssetSize: 100 * 1024, // 100 KiB 
+   maxEntrypointSize: 100 * 1024, // 100 KiB
+ },
  // ...
}
```

We defined 100KB as a threshold for assets (e.g., CSS files) as well as entry points (i.e., generated JS bundles). In contrast to the previous screenshot (`warning`), here we define  `hints` as type `error`.

As we see in our output, this means that the build will fail. With `warning`, however, the build will still succeed.

![Failed Build Output With Errors](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/build-output-errors.png?resize=730%2C750&ssl=1)

webpack offers advice on how to deal with the problems.

I like to set `hints: "error"` for the build. This causes your CI pipeline to fail if you push it to VCS anyway. In my opinion, this promotes a culture of caring about bundle/asset sizes and, ultimately, user experience early on.

## Use tree-shakeable third-party libraries

Let’s inspect our bundle to see how much space our popular Lodash dependency consumes in contrast to the other components. Check out out branch `master` and run `npm run build:analyze`.

![Examining The Size Of Lodash In The Bundle Analyer](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/bundle-analyzer-lodash.png?resize=730%2C733&ssl=1)

That’s a pretty big chunk not only in absolute terms, but also in relation to the other components. This is how we use it.

```jsx
// Profile.js
import _ from "lodash";
// ...
const Profile = () => {
  const array = [1, 2, 3];
  _.fill(array, "a");
  console.log(array);
  // ...
}
```

The problem is that we pull in the whole library even though we just use the `fill` function. Luckily, Lodash has a modular architecture and also supports tree shaking.

We just need to change the import to pull in only the array functions (check out branch [`lodash-modular`](https://github.com/doppelmutzi/react-performance-strategies/tree/lodash-modular)).

```jsx
// Profile.js
- import _ from "lodash";
+ import _ from "lodash/array";

// ...
```

The build output looks much better now.

![Lodash Array Functions In The Bundle Analyzer](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/bundle-analyzer-lodash-modular.png?resize=730%2C731&ssl=1)

The bottom line is that you should pay particular attention to your libraries when you inspect your bundles. If you use only a small portion of the total library scope, something is wrong. Check the library’s documentation to see whether you can import individual functionalities. Otherwise, look for modularized alternatives or roll your own solution.

We’ve now learned all but one of the requirements that must be met to have tree shaking in place, which is that external libraries need to tell webpack they don’t have side effects.

From webpack’s perspective, all libraries have side effects unless they tell it otherwise. Library owners have to mark their project, or parts of it, as [side effect-free](https://webpack.js.org/guides/tree-shaking/#mark-the-file-as-side-effect-free). Lodash also does this with the `sideEffects: false` property within its [`package.json`](https://github.com/lodash/lodash/blob/master/package.json).

## Ship the correct variant of source maps to production

It’s up to you and your team to decide if you want to [ship source maps to production](https://css-tricks.com/should-i-use-source-maps-in-production/), whether for debugging reasons or to enable others to learn from you.

If you don’t want source maps at all for production bundles, you need to make sure no [devtool configuration option](https://webpack.js.org/configuration/devtool/) is set for production mode. But if you do want to have source maps for production, take care that you use the right `devtool` variant because it has an effect on webpack’s build speed and, most importantly, on the size of the bundles.

For production, `devtool: "source-map"` is a good choice because only a comment is generated into the JS bundle. If your users do not open the browser’s devtools, they do not load the corresponding source map file generated by webpack.

For adding source maps, we just have to add one property to the production configuration (check out branch [`sourcemaps`](https://github.com/doppelmutzi/react-performance-strategies/tree/sourcemaps)).

```jsx
// webpack.prod.js
const config = {
  mode: "production",
+ devtool: "source-map",
  // ...
}
```

The following screenshot shows how the result would look for the production build.

![Production Build Output With Source Maps](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/production-build-output-source-maps.png?resize=730%2C407&ssl=1)

webpack only added one line of comment in every asset.

![Source Map Comment Example](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/source-map-comment.png?resize=730%2C139&ssl=1)

In contrast, when you run the development build with `devtool: "inline-source-map"`, you can clearly see why it’s not a good idea to use this configuration for production.

![Build Output With inline-source-map Specified](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/build-output-inline-source-map.png?resize=730%2C162&ssl=1)

## Preparing for long-term caching

Check out the branch [`caching-no-optimization`](https://github.com/doppelmutzi/react-performance-strategies/tree/caching-no-optimization) and run a production build to see the starting point.

![Production Build Output Of The caching-no-optimization Branch](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/production-build-output-caching-no-optimization.png?resize=730%2C185&ssl=1)

Why is this result problematic? From a caching standpoint, the asset names are suboptimal. We should configure our web server to respond with [`Cache-Control`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) headers that set appropriately large `max-age` values to support long-term browser caching.

With these filenames, however, we cannot invalidate browser-cached assets after webpack has generated new versions due to developer activity, like bug fixes, a new feature, etc. As a result, our users may end up using stale and buggy JavaScript bundles even though we already provided updated versions.

We can do better. We can use placeholder strings in different places in our webpack configuration — in the `filename` value, for example. We use `[hash]` to create a unique filename for an asset whenever we’ve made a change to the corresponding source files (check out branch [`caching-hash`](https://github.com/doppelmutzi/react-performance-strategies/tree/caching-hash)). We shouldn’t overlook our CSS assets, either.

```jsx
// webpack.prod.js
const config = {
  // ...
  output: {
-    filename: "static/[name].js",
+    filename: "static/[name].[hash].js",
  },
  // ...
  plugins: [
    new MiniCssExtractPlugin({
-      filename: "styles/[name].css",
+      filename: "styles/[name].[hash].css",
    }),
    // ...
  ],
}
```

![Build Output With Hash Values In Filenames](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/build-output-hash-values.png?resize=730%2C163&ssl=1)

Cool, we have fancy hash values as part of our filenames. But not so fast — we’re not done yet. The result shows that all assets have the same hash value. This doesn’t help at all because whenever we change one file, all the other assets would receive the same hash value.

Let’s change `Profile.js` by adding a `console.log` statement and run the build again.

```jsx
// Profile.js
const Profile = () => {
+ console.log("hello world");
  // ...
};
export default Profile;
```

As expected, all assets get the same new hash value.

![All Assets Have The Same Hash Value](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/build-output-same-hash-values.png?resize=730%2C160&ssl=1)

That’s because we have to use a different filename [substitution placeholder](https://webpack.js.org/guides/caching/#output-filenames). In addition to `[name]`, we have three placeholder strings when it comes to caching:

- `[hash]` – if at least one chunk changes, a new hash value for the whole build is generated
- `[chunkhash]` – for every changing chunk, a new hash value is generated
- `[contenthash]` – for every changed asset, a new hash based on the asset’s content is generated

At first, it’s not quite clear what the difference is between `[chunkhash]` and `[contenthash]`. Let’s try out `[chunkhash]` (check out branch [`caching-chunkhash`](https://github.com/doppelmutzi/react-performance-strategies/tree/caching-chunkhash)).

```jsx
// webpack.prod.js
const config = {
  // ...
  output: {
-    filename: "static/[name].[hash].js",
+    filename: "static/[name].[chunkhash].js",
  },
  // ...
  plugins: [
    new MiniCssExtractPlugin({
-      filename: "styles/[name].[hash].css",
+      filename: "styles/[name].[chunkhash].css",
    }),
    // ...
  ],
}
```

Give it a shot and run the production build again!

![Corresponding CSS And JS Assets Have Matching Hashes](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/css-js-assets-matching-hashes.png?resize=730%2C160&ssl=1)

This looks a little better, but the hashes of the CSS assets match with their corresponding JS assets. Let’s change `Profile.js` and hope the corresponding CSS filename won’t be updated.

```jsx
// Profile.js
const Profile = () => {
+  console.log("hello world again");
  // ...
};
```

This result is even worse. As you can see in the screenshot below, not only does the `Profile.css` filename include the same new hash value as the new `Profile.js`, but both `App.js` and `App.css` were also updated with another new hash value.

![Changing Profile.js Results In New Hashes For Other Assets](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/change-profile-js-same-hashes.png?resize=730%2C160&ssl=1)

Maybe `[contenthash]` helps? Let’s find out (check out branch [`caching-contenthash`](https://github.com/doppelmutzi/react-performance-strategies/tree/caching-contenthash)).

```jsx
// webpack.prod.js
const config = {
  // ...
  output: {
-    filename: "static/[name].[chunkhash].js",
+    filename: "static/[name].[contenthash].js",
  },
  // ...
  plugins: [
    new MiniCssExtractPlugin({
-      filename: "styles/[name].[chunkhash].css",
+      filename: "styles/[name].[contenthash].css",
    }),
    // ...
  ],
}
```

Each asset has been given a new but individual hash value.

![Branch caching-contenthash Build Output](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/caching-contenthash-build-output.png?resize=730%2C160&ssl=1)

Did we make it? Let’s change something in `Profile.js` again and find out.

```jsx
// Profile.js
const Profile = () => {
+  console.log("Let's go");
  // ...
};
```

CSS asset names have not changed, so we have decoupled them from the associated JS assets.

![Build Output With Decoupled CSS And JS Assets](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/build-output-decoupled-css-js.png?resize=730%2C160&ssl=1)

The vendor bundle still has the same name. Since the `Profile` React component is imported into the `Blog` React component, it’s OK that both filenames for both JS bundles have changed.

A change in `Blog.js`, though, should only lead to a change of the `app` bundle name and not of the `profile` bundle name because the latter imports nothing from the former.

```jsx
// Blog.js
export default function Blog() {
+  console.log("What's up?");
  // ...
}
```

This time, the profile bundle name is unchanged — great!

![The Profile Bundle Name Is Now Unchanged](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/profile-bundle-name-unchanged.png?resize=730%2C159&ssl=1)

In addition, the [official webpack doc](https://webpack.js.org/guides/caching/)[s](https://webpack.js.org/guides/caching/) recommend you also do the following:

- Extract the manifest in a separate bundle
- Use [`moduleIds`](https://webpack.js.org/configuration/optimization/#optimizationmoduleids)

Check out branch [`caching-moduleids`](https://github.com/doppelmutzi/react-performance-strategies/tree/caching-moduleids) for the final version of our configuration.

```jsx
// webpack.prod.js
const config = {
  // ...
  optimization: {
+   moduleIds: "hashed",
+   runtimeChunk: {
+     name: "manifest",
+   },
    splitChunks: {
      // ...
    }
  }
  // ...
}
```

Now all the JS bundles have changed because the manifest was extracted out of them into a separate bundle. As a result, their sizes have shrunk a tiny bit.

![Build Output For The caching-moduleids Branch](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/caching-moduleids-build-output.png?resize=730%2C191&ssl=1)

I’m not sure when the manifest changes — maybe after a webpack version update? But we can verify the last step. If we update one of the vendor dependencies, the `node_vendors` bundle should get a new hash name, but the rest of the assets should remain unchanged. Let’s change a random dependency in `package.json` and run `npm i && npm run build`.

![The Build Output After Updating The node_vendors Bundle](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2020/05/build-output-updated-vendors-bundle.png?resize=730%2C188&ssl=1)

It was hard work, but it was worth it. Now we are better prepared for cache busting.

## Conclusion

Performance optimization is a complicated matter, and webpack makes it a little less complicated.

The key to integrate performance optimization into your project is to understand some basic concepts, such as tree shaking, code splitting, and cache busting. Sure, webpack is an opinionated module bundler, but with a good understanding of the underlying principles, it is easier to work with the webpack documentation and achieve your desired performance goals.

This guide focuses mainly on performance optimization techniques regarding JavaScript. Some HTML was also included with the `HtmlWebpackPlugin`. There are more areas that could be covered, such as optimizing styles (e.g., CSS-in-JS, critical CSS) or assets (e.g., brotli compression), but that would be the subject of another article.

* * * 
## Plug: [LogRocket](https://logrocket.com/signup/), a DVR for web apps
 
![LogRocket Dashboard Free Trial Banner](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png?resize=1200%2C677&ssl=1)
 
[LogRocket](https://logrocket.com/signup/) is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
 
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
 
[Try it for free](https://logrocket.com/signup/).
* * *

The post [An in-depth guide to performance optimization with webpack](https://blog.logrocket.com/guide-performance-optimization-webpack/) appeared first on [LogRocket Blog](https://blog.logrocket.com).