6 tips to optimize bundle size
Tags: webpack, react, javascript, performance
Published: 2020-05-13 14:56:51

Disclaimer:
> This article includes optimizations that are only valid for an app bundled with webpack, or only for a React app. Some optimizations also assume you have control over the webpack configuration of your app.

# Measure and visualize bundle size
_Valid for: any app bundled with Webpack_

The tool [webpack-bundle-analyzer](https://www.npmjs.com/package/webpack-bundle-analyzer) can produce an easily understandable view of the composition of a JS bundle.

The easiest way to use it is to generate a stats file with webpack and to launch the tool with `npx`.

```bash
webpack --profile --json > stats.json
# Assuming generated bundled files are in the dist folder
npx webpack-bundle-analyzer stats.json dist/  
```


<figure>
![Example of a webpack-bundle-analyzer tab](https://dev-to-uploads.s3.amazonaws.com/i/7v64iizv17wrf2qy18q9.png)
<figcaption>webpack-bundle-analyzer launches a tab in your browser which look like this</figcaption>
</figure>

To understand what the different sizes mean:
- `Stat size` is the size of the input, after webpack bundling but before optimizations like minification
- `Parsed size` is the size of the file on disk (after minification). It is the effective size of the JavaScript code parsed by the client browser
- `gzip size` is the size of the file after gzip (most likely the effective content size transmitted over the network)

# 1. Avoid libraries global imports

_Valid for: any bundled app or lib_
_Cost: Low_
_Impact: High_

With some large libraries, it is possible to import only the parts we use instead of the entire library. If done right, this can save a lot of unused bundle size.

Examples of splittable libs: lodash, date-fns, react-bootstrap...

The downside of this is that only one global import in the app or in dependencies that transitively depend on these libs is necessary to make the bundler include the whole dependency.

<figure>
<img src="https://dev-to-uploads.s3.amazonaws.com/i/zhdf4bodb3xql6vpzpsx.png" alt="Using webpack-bundle-analyzer a global import of lodash will look like this" />
<figcaption>Using webpack-bundle-analyzer, a global import of lodash will look like this</figcaption>
</figure>

In this capture you can see that necessary functions are bundled up to 3 times (once in lodash.js, once in lodash.min.js, and once in one-by-one imports). This is the worse case scenario.

There are two ways to enforce one-by-one imports. Note that both of these methods don't apply to dependencies, only to your own code.

## Via a babel plugin

The plugin [babel-plugin-transform-imports](https://www.npmjs.com/package/babel-plugin-transform-imports) has the ability to replace global destructured imports by one-by-one imports.

Configured like this:

```json
# .babelrc
"plugins": [
  ["transform-imports", {
    "lodash": {
      "transform": "lodash/${member}",
      "preventFullImport": true
    }
  }]
]
```

It will have the following effect:

```jsx
import { map, some } from 'lodash'
// will be replaced by
import map from 'lodash/map'
import some from 'lodash/some'
```

Note that the option `preventFullImport` will tell the plugin to throw an error if it encounters an import which would include the entire library.

## Via an ESLint rule

The downside of the first method is that two methods of imports of the same function are valid, meaning it becomes harder to enforce a single import style in a single project.

Fortunately it is possible to configure the [no-restricted-imports](https://eslint.org/docs/rules/no-restricted-imports) rule to throw an error if a global import is encountered.

```json
// .eslintrc
"no-restricted-imports": [
  "error",
  {
    "paths": [
      "lodash"
    ]
  }
]
```

It will produce an error for the following import:

```js
import { map } from 'lodash'
```
But not for a specific module import like this.

```js
import map from 'lodash/map'
```

You can of course combine these two methods to enforce a specific style.

## 2. Use code-splitting
_Valid for: Web apps bundled with webpack_
_Cost: Low_
_Impact: Variable_

Using dynamic imports and Suspense, it is possible to split application code in async chunks that can be loaded on-demand. This allows to reduce the size of the bundle downloaded initially. It does not reduce overall bundle size (it even slightly increases it).

Configuration:
```js
# webpack.config.js
optimization: {
  splitChunks: {
    // include all types of chunks
    chunks: 'all',
  }
}
```

By default a “vendors” chunk is created, separating application code from dependencies. This can have a positive impact when updating the application. If only the application code changes (assuming resources are correctly cached), the client can save the cost of downloading vendors files. This behaviour can be disabled by setting: 

```js
optimization: {
  splitChunks: {
    // include all types of chunks
    chunks: 'all',
    cacheGroups: {
      vendors: false,
    },
  },
}
```

Be careful not to be overzealous with code splitting as this can slow down some user actions as we have to download, parse and execute more code. Depending on the structure of the application, it possible that adding a chunk implies downloading several files (with HTTP 1, [there is a limit on parallel connections to the same domain](https://stackoverflow.com/a/14768266/3890766)).

**The recommended way is to create a chunk per route**. This is **not** an absolute rule.

How to export a lazy loaded component:

```jsx
// myComponent.lazy.jsx
import React, { Suspense } from 'react'
import LoadingIndicator from '..'
 
// Create a lazy component using React.lazy
export const MyComponentLazy = React.lazy(() =>
 import(/* webpackChunkName: "my-component" */ './myComponent'),
)

const MyComponent = props => (
 <Suspense fallback={<Loading Indicator />}>
   <MyComponentLazy {...props} />
 </Suspense>
)

export default MyComponent
```

Here we use the dynamic import syntax to tell Webpack to bundle a separate chunk for MyComponent (and all its dependencies). 

Setting the `webpackChunkName` is optional, this allows to control the name of the generated file (with the [corresponding webpack configuration](https://webpack.js.org/configuration/output/#outputchunkfilename)). If two lazily imported components have the same name, they will be concatenated in a single chunk.

`React.lazy` is used to allow the lazily imported component to be rendered like a regular component. `Suspense` allows to provide a fallback (component that will be rendered while the import is not resolved).
Note that `Suspense` can be further up the component tree, depending on what the users should see during load.

See [React documentation](https://en.reactjs.org/docs/code-splitting.html) for a more complete explanation of `lazy` and `Suspense`.

# 3. Do not include source maps

_Valid for: Web apps and libs bundled with Webpack_
_Cost: Low_
_Impact: Variable_

Source maps are a link between source code and generated bundled files. While it can be really useful to use browser debuggers, it shouldn't be included in the production bundle.

For JS source-map, the option [devtool](https://webpack.js.org/configuration/devtool/#root) controls how source-maps are generated.
For development, `'eval-source-map'` is a good choice (we see the original source and rebuilds are fast).
For production, setting `false` will completely disable source-map generation. As it can be useful to debug generated bundled app, the best way to keep them in production is to set `devtool: 'source-map'`. It will generate a separate file (downloaded only if browser devtools are open) linked by a comment added in the original bundle that looks like this: `//# sourceMappingURL=app.daa9676c2167d965d0ae.js.map`.

For CSS, Less or Sass source-maps, the configuration depends on the loader used. Using [css-loader](https://github.com/webpack-contrib/css-loader), [sass-loader](https://github.com/webpack-contrib/sass-loader) and [less-loader](https://github.com/webpack-contrib/less-loader), I would recommend setting `options: { sourceMap: true }` in development inside loader configuration, and `options: { sourceMap: false }` in production (as this is the default, you can safely omit the property in production).

# 4. Remove replaceable libs

_Valid for: any bundled app or lib_
_Cost: Variable_
_Impact: Variable_

It can be very tempting to add a library which does meet the user requirement but also do much more. Possible reasons include not knowing future needs of users or simply to deliver faster.

Adding unneeded complexity can have a huge impact on the bundle size.

In my project, we found out that we used [libphonenumber-js](https://www.npmjs.com/package/libphonenumber-js) for only two use cases:
- Format a french phone number
- Validate an input field which only allows french phone numbers

Having to deal only with french phone number greatly reduce the complexity needed for these kind of features. libphonenumber-js is a great library -- just not fitted to our needs.

Rewriting these feature using only vanilla JS took only a few hours and saved us ~150 KiB of JS bundle size.

For each dependency, you should wonder:
- Do we use only a small part of the dependency ?
- Do we have the capacity to rewrite it in a reasonable time ?

If the answer to both questions is _yes_, it seems that rewriting code which meet the needs of the project (and only them) is a good idea.

# 5. Remove prop-types
_Valid for: React apps_
_Cost: low_
_Impact: High_

With React, defining prop-types enables validation of props passed to a component. Although it is really useful in development, prop-types are disabled in production (mostly for performance reasons).

But their definition is still included in the produced bundle.

The Babel plugin [transform-react-remove-prop-types](https://github.com/oliviertassinari/babel-plugin-transform-react-remove-prop-types#readme) completely deletes prop-types definitions from the generated bundle. However, prop-types included by dependencies are not removed.

```json
// .babelrc
{
  "env": {
    "production": {
      "plugins": [
        [
          "transform-react-remove-prop-types",
          {
            "removeImport": true
          }
        ]
      ]
    }
  }
}

```

**Warning**: only activate this plugin in the production environment.

## 6. Target recent browsers

_Valid for: any web app_
_Cost: low_
_Impact: medium_

To include polyfills, you probably already use [core-js](https://github.com/zloirock/core-js) and [regenerator-runtime](https://www.npmjs.com/package/regenerator-runtime).

By default, all polyfills are included and core-js weights approximately 154KiB while regenerator-runtime is only 6.3KiB.

By targeting only recent browsers, it is possible to reduce the size of included polyfills. 

[Babel-preset-env](https://babeljs.io/docs/en/babel-preset-env) has the ability to replace global imports of core-js by specific imports dependings on the targeted browsers.

To configure the preset:
```json
// .babelrc
"presets": [
  [
    "@babel/preset-env",
    {
      "useBuiltIns": "entry",
      "corejs": "3.6"
    }
  ],
],
```
The version of core-js has to be provided.

With `"useBuiltIns": "entry"`, you only have to import these two dependencies once:

```js
import 'regenerator-runtime/runtime'
import 'core-js/stable'
```

These two imports will be replaced by specific imports depending on the targeted browsers.

To declare targeted browsers, the preset uses the [browserslist](https://github.com/browserslist/browserslist) syntax.

```json
"browserslist": "last 2 Chrome versions, last 2 Firefox versions, last 2 safari versions",
```
## Conclusion

Hope this helps !

Let me know if this is useful to you and how much bandwidth and client memory you saved