React/Webpack: From MB to KB. How we solved our bundling problem
Tags: react, webpack, javascript
Published: 2021-10-20 17:50:54

> This article was first posted on [Sovrn Tech Blog](https://tech.sovrn.com/2021/02/22/react-webpack-bundling.html).

In this article I am going to describe the mistakes we made in bundling our UI application written in React.

The reasons we reached serving a bundle > `11.0mb` for a relative small application and the steps we took in order to minimize and split that bundle for better loading performance.

## Some Context

Our UI is a React application where we use [Webpack](https://webpack.js.org/) for bundling our application, [Material-UI](https://mui.com/) as a design system for our components and an internal library which is a wrapper of [Material-UI](https://mui.com/) that we use across the company for creating a cohesive and consistent brand identity in the UI. Finally we have split our application to smaller independent npm modules which we pull into our main UI like “plugins”.

Bundling never has been an issue or at least noticeable enough that we had to take action. But after a extensive updates in all of our dependencies

- `Webpack v4` to `Webpack v5`
- `React 16.8.x` to `React 16.14.x`
- `Material-UI v3` to `Material-UI v4`
- … and more

we started noticing our application was taking more time to load and was slower in a “cold start”.

> With the term “cold start” I mean we haven’t used the application for a long time and when we visit our browser doesn’t have any resources cached.

&nbsp;

## The problem

Our first action was to visit Chrome Devtools and inspect what was slowing us down

![Chrome Devtools Network Tab](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c88np4fu6q2x51t1r45o.png)

> Time here is not representative as the screenshot is from local served instance

So we noticed the bundle was much bigger but we couldn’t understand what was different as our implementation remained the same so we should not be pulling more dependencies into our bundle.

&nbsp;
 
## The solution(s)

We started by analyzing our bundle and understanding what exactly was delivered to the user. We found a webpack plugin that helped us to do this:

[Webpack Bundle Analyzer](https://www.npmjs.com/package/webpack-bundle-analyzer) - “Visualize size of webpack output files with an interactive zoomable treemap.”

![Webpack Bundle Analyzer TreeMap](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r7pcblcxnzprq6x1ezj4.png)

&nbsp;

From this image we could right away understand that multiple things were wrong

1. As you can see we were having multiples instances of the same library being pulled from different dependencies. For example `underground-ui-whitespace-sovrn-content`, `underground-ui-sync-skys-services-content`, etc, all those modules are the “plugins” I mentioned above, and they all have a copy of the `Material-UI` even if `Material-UI` is present in the main application. The same thing happened with React as well.

2. Another issue was some “heavy” libraries we were not really utilizing to excuse relying on them, e.g. [Moment.js](https://momentjs.com/), [Bluebird](http://bluebirdjs.com/docs/getting-started.html), [Lodash](https://lodash.com/).

3. Last mistake that was noticeable just from this view was that we were not tree shaking. It’s evident from `Material-UI` icons section we were importing **all** the icons. 

Now we had a plan.

### Peer dependencies and versioning

For the first issue we reviewed all of our internal UI “plugins” and we found that in our dependencies most of the duplicated libraries were locked in specific versions. By doing so, mistakenly were declaring that our “plugin” could only work with this specific version so we ended with different versions of the same library.

The solution was using `peerDependencies` and using ^ syntax in our versions.

> ^ in semantic versioning means we accept all minor releases ( e.g 1.x ) and not a specific one.

> Peer dependency means that your package needs a dependency that is the same exact dependency as the person installing your package.

So now the main application was responsible for providing the dependencies to the “plugins” for running.

### "Heavy" libraries

Second step was removing the “heavy” libraries, it was easy removing [Moment.js](https://momentjs.com/), [Bluebird](http://bluebirdjs.com/docs/getting-started.html). We replaced the first with [date-fns](https://date-fns.org/) and [Bluebird](http://bluebirdjs.com/docs/getting-started.html) with native promises. [Lodash](https://lodash.com/) unfortunately because of time constraints we could not refactor into moving out from some “handy” utilities it provides but we are planning to.

### Tree shaking

> Tree shaking is a term commonly used in the JavaScript context for dead-code elimination.

Third step was tree shaking and needed more investigation. So we started by reading for [Material-UI Minimizing Bundle Size](https://mui.com/guides/minimizing-bundle-size/) and how to `import` for shaking [Material-UI](https://mui.com/) components and icons but we could not find something wrong there. So our next option was [Webpack Tree Shaking](https://webpack.js.org/guides/tree-shaking/). Lot’s of interesting points there but the one we needed was this

> It relies on the static structure of ES2015 module syntax, i.e. import and export.

but we were compiling our own modules and the main UI to `module: commonjs` and `target: es5` so Webpack was not able to understand what was “dead code” and should be tree shaken. So we changed to compile into `module: esnext` and `target: es2017`.

&nbsp;

![TreeMap after changes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hjtxt8c10pug7felw36f.png)_The results were **amazing**_
 
&nbsp;

We dropped from the `11.0mb` to `4.67mb` without losing any functionality but still something was not right. The module in the screenshot `@sovrn/platform-ui-core` is the wrapper we use around [Material-UI](https://mui.com/) and we could see some components that we were clearly not using. We went back did some reading and found the `sideEffects` property in `package.json` that [Webpack](https://webpack.js.org/) has adopted for - *denoting which files in a project are “pure” and therefore safe to prune if unused*. [Material-UI](https://mui.com/) uses it but we didn’t so we were not able to tree shake our internal [Material-UI](https://mui.com/) wrapper.

> For more information about `sideEffects` [Clarifying tree shaking and sideEffects](https://webpack.js.org/guides/tree-shaking/#clarifying-tree-shaking-and-sideeffects).

&nbsp;

![Final TreeMap](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pxxua8vhuezxyyu5n171.png)
*So the bundle after this change was `3.34mb` and `269kb` Gzipped*

&nbsp;

## Bonuses

Of course after so much investigation we identified other places were we could improve our application.

### Code Splitting

Our application is structured in a way that can be code split ( “plugin” components ). So we leveraged [Webpack Code Splitting](https://webpack.js.org/guides/code-splitting/) and [React Code Splitting](https://reactjs.org/docs/code-splitting.html) with `lazy loading` so we load the bundles for the plugins only when we need them.

&nbsp;

![TreeMap with codesplitting](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wqiks0ki8pxrpzbc1dnf.png)

*the final bundle looks like this*

So now on our initial load we only pull dependencies and bundles used for the initial scene meaning we are pulling a bundle of `~1.9mb` or `~173kb` Gzipped.

All the colorful modules are our “plugins” that can be dynamically loaded on request.

### How to keep track

Last but not least, we wanted to make sure we could keep track of our bundle and make sure that every time we introduce a new change we can see how it affects our bundle.

There are many tools you can use and integrate to your CI/CD pipeline. We use [Bundlesize](https://www.npmjs.com/package/bundlesize), which you can configure it and set limits for your bundlesize and if the build isn’t below those limits it will fail.

```bash
...
 PASS  dist/static/js/140.39a3af3a.js: 171.73KB < maxSize 244KB (gzip)

 PASS  dist/static/js/201.e6df94bb.chunk.js: 3.33KB < maxSize 244KB (gzip)

 PASS  dist/static/js/218.9e0f9972.chunk.js: 2.47KB < maxSize 244KB (gzip)

 PASS  dist/static/js/246.1c66cc41.chunk.js: 3.49KB < maxSize 244KB (gzip)
...
```

So in conjunction with [Webpack Bundle Analyzer](https://www.npmjs.com/package/webpack-bundle-analyzer) we can know what’s wrong in our bundle or not.

&nbsp;

---

If you liked or found the post useful leave a ❤️