Minimizing Lodash Bundle Size in CRA and Next.js
Tags: react, webpack, nextjs, babel
Published: 2021-08-28 12:28:26


When developing an application, we would usually write some utility functions that could be reused throughout the application. 

An example

```javascript
//--------------
// utils.js

function sum(a, b){
    return a + b;
}

function subtract(a, b){
    return a - b;
}

export {
    sum,
    subtract
}

//---------------
// component.js

import {sum, subtract} from 'utils.js';

function calculate(a, b){
    return sum(a, b) + subtract(a, b);
}
```

## What is Lodash?

Lodash is a package that provides a ton of utilities to help us with manipulating the data we have. It has implementations like _clone, deepClone, isNil, every, sum_ etc. 

In a large application, you would import the utilities from lodash like this and use it

```jsx
import { sum } from "lodash";

<Button onClick={() => {
    console.log(sum([1, 2, 3, 3]));
}}> Sum of Array </Button>
```

When we are doing this _named import_ from lodash, we are actually importing the whole lodash.js file and using just the sum function from it. 

```javascript
import { sum } from "lodash";

// would be converted to
var lodash = import('lodash');

// and then accessing sum property from lodash
lodash.sum([1, 2, 3, 3])
```

So, with the named exports it is not possible for the bundlers like webpack to treeshake the unwanted code, and we 
end up shipping entire lodash utilities to the user. 

You can avoid this by directly importing the utility you need from lodash like this

```javascript
import sum from "lodash/sum";
import cloneDeep from "lodash/cloneDeep";
import throttle from "lodash/throttle";
...
```

But this can be tough to maintain if you are using a lot of utilities from lodash in a file and you would have a bunch
of `import` statements at the top.

Thankfully with babel we have a plugin called [babel-plugin-transform-imports](https://www.npmjs.com/package/babel-plugin-transform-imports), which, if configured, can transform our named imports to default file imports.

All you have to do is install babel-plugin-transform-imports

```bash
npm install --save-dev babel-plugin-transform-imports
```

and configure babel like this

```javascript
// pacakge.json or .babelrc file

"babel": {
    "plugins": [
        [
            "babel-plugin-transform-imports",
            {
                "lodash": {
                    "transform": "lodash/${member}",
                    "preventFullImport": false
                }
            }
        ]
    ]
}
```

What this essentially does is

```javascript
import { sum } from "lodash";

// would be converted by babel on build step to
import sum from "lodash/sum";

// then on the webpack chunk
var lodash_sum = import('lodash/sum.js');

// and then
lodash_sum([1, 2, 3, 3])
```

Which results in a smaller bundle size!

## Let's Measure It

The significant step that we need to do while carrying out performance optimizations is to measure it. 

We need to measure the cost before and after introducing an optimization.

If we aren't measuring it, one little mistake with the optimization would cost us additional performance hit than what was before! So, the rule of the thumb is

> Don't do performance optimizations without measuring it.

Let's create a react app

```bash
npx create-react-app fresh-cra
```

I am using latest version of [create-react-app](https://create-react-app.dev/docs/getting-started/), with the below packages

```javascript
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3"
```

I am going to install lodash

```bash
npm install lodash
```

Then, I am going to modify the `App.js` to include the `sum` function

```jsx
import "./App.css";
import { sum } from "lodash";

function App() {
  return (
    <div className="App">
      <button
        onClick={() => {
          console.log(sum([1, 2, 3, 3]));
        }}
      >
        Sum of Array
      </button>
    </div>
  );
}
```

Now, let's [analyse the bundle size](https://create-react-app.dev/docs/analyzing-the-bundle-size).

For this, we would use the package [source-map-explorer](https://www.npmjs.com/package/source-map-explorer)
which gives a nice visualisation of the bundles that we ship to the users.

```
npm install --save source-map-explorer
```

Add a new script in package.json

```javascript
"scripts": {
+   "analyze": "source-map-explorer 'build/static/js/*.js'",
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
```

I am going to build the app to create a `/build` folder

```bash
$ npm run build 

Compiled successfully.
File sizes after gzip:

  65.17 KB  build/static/js/2.ab4556c4.chunk.js
  1.63 KB   build/static/js/3.49b2ab04.chunk.js
  1.17 KB   build/static/js/runtime-main.97cb050d.js
  574 B     build/static/css/main.9d5b29c0.chunk.css
  469 B     build/static/js/main.c3c1410a.chunk.js
```

Now, I am going to run `analyze`

```bash
npm run analyze
```

Running this command would open a tab in the default browser with the bundle details.

![Bundle size before optimization](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dld252mehr6wthk7u12w.png)

If the image is too small, the box which has text underlined in red is the size of lodash we are serving to users. We are shipping ~70 KB of lodash package just for using a simple `sum` utility. Accounts for about 35% of total bundle size 🤯

## Let's optimize it

I am going to install [babel-plugin-transform-imports](https://www.npmjs.com/package/babel-plugin-transform-imports)

```bash
npm install --save-dev babel-plugin-transform-imports
```

In order to tweak babel configuration without [ejecting](https://create-react-app.dev/docs/available-scripts#npm-run-eject) from create-react-app, we need to add additional
packages.

```bash
npm install -D react-app-rewired customize-cra
```

Then create a `config-overrides.js` file in your root directory with this code

```javascript
/* config-overrides.js */
const { useBabelRc, override } = require("customize-cra");

module.exports = override(
  // eslint-disable-next-line react-hooks/rules-of-hooks
  useBabelRc()
);
```

Replace `react-scripts` with `react-app-rewired` in package.json

```javascript
- "start": "react-scripts start",
- "build": "react-scripts build",
+ "start": "react-app-rewired start",
+ "build": "react-app-rewired build",
```

The override setup is completed. Now, we can configure babel in create-react-app!

To do that, create a .babelrc file in root directory and use the following code

```javascript
{
  "plugins": [
    [
      "babel-plugin-transform-imports",
      {
        "lodash": {
          "transform": "lodash/${member}",
          "preventFullImport": true
        }
      }
    ]
  ]
}
```

Now that we are all set, let's run the build

```bash
$ npm run build

Compiled successfully.
File sizes after gzip:

  41.41 KB (-23.75 KB)  build/static/js/2.39f2f9c9.chunk.js
  1.63 KB               build/static/js/3.49b2ab04.chunk.js
  1.17 KB               build/static/js/runtime-main.97cb050d.js
  574 B                 build/static/css/main.9d5b29c0.chunk.css
  472 B (+3 B)          build/static/js/main.9d111c34.chunk.js
```

then 

```bash
npm run analyze
```

💥 Boom! 💥

![Bundle size after optimization](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bvilt9ajanp5fqchlyu7.png)

_In the above image, we could not see the lodash box that we saw earlier because of optimization_


It looks like lodash is gone! Actually, it's not. Since the cost of `sum` utility is so small, our _source-map-explorer_ isn't showing that up. Right after the build command, you can see the reduction of chunk size by 23 KB.

## Let's explore further

If you are satisfied with the optimization by looking at the output from _source-map-explorer_ then we are good. If you are not, and you are really curious to see the babel magic, let's explore further.

First, let's go back to the state of our application before optimization. 

This is simple for us, just replace `react-app-rewired` with `react-scripts` 

```javascript
- "build": "react-app-rewired build",
+ "build": "react-scripts build",
```

Now, to see the babel transpilation, go to `webpack.config.js` file in `node_modules` directory and look for the object `optimization` with the key `minimize` then make that as `false`

```javascript
// node_modules/react-scripts/config/webpack.config.js

...
return {
    ...
    optimization: {
        minimize: false, //isEnvProduction,
        ...
    }
}
```

This would stop the minification of our source code by webpack, so that we can understand what is shipped to the end-user.

Now, run the build command.

```bash
$ npm run build

Compiled successfully.

File sizes after gzip:

  142.77 KB (+77.6 KB)  build/static/js/2.b2a9563e.chunk.js
  2.48 KB (+1.31 KB)    build/static/js/runtime-main.51b24467.js
  2.06 KB (+441 B)      build/static/js/3.8a130f73.chunk.js
  960 B (+491 B)        build/static/js/main.1ad88ea0.chunk.js
  625 B (+51 B)         build/static/css/main.9d5b29c0.chunk.css
```

The first thing you can observe here is the increase in bundle size! This is because we are no longer minifying our code.

Go into the `/build/static/js` folder and open the main chunk (main.[HASH].chunk.js). 

Search for `lodash`, and you will see the below code

```javascript
...
// EXTERNAL MODULE: ./node_modules/lodash/lodash.js
var lodash = __webpack_require__(4);

...

// sum is accessed as a property from lodash object
("button",{onClick:function onClick(){console.log(Object(lodash["sum"])([1,2,3,3]));},children:"Sum of Array"})
...
```

The one on line number 8 is the transpiled JSX that we wrote. 

> I have already explained about the JSX transpilation in the post [React is Just Javascript](https://www.bharathikannan.com/blog/react-is-just-javascript)

Now, let's do the optimization

```javascript
- "build": "react-scripts build",
+ "build": "react-app-rewired build",
```

and run the build

```bash
$ npm run build

Compiled successfully.

File sizes after gzip:

  49.64 KB (-93.13 KB)  build/static/js/2.08144287.chunk.js
  2.48 KB               build/static/js/runtime-main.51b24467.js
  2.06 KB               build/static/js/3.8a130f73.chunk.js
  965 B (+5 B)          build/static/js/main.22f99645.chunk.js
  625 B                 build/static/css/main.9d5b29c0.chunk.css
```

Let's check the transpiled chunk (`/build/static/js/main.22f99645.chunk.js`)

```javascript
// EXTERNAL MODULE: ./node_modules/lodash/sum.js
var sum = __webpack_require__(4);
var sum_default = /*#__PURE__*/__webpack_require__.n(sum);

// default sum is called
("button",{onClick:function onClick(){console.log(sum_default()([1,2,3,3]));},children:"Sum of Array"})
```

Did you notice the difference in the code that we shipped? 

Instead of importing everything from lodash.js we are now importing from lodash/sum.js.

Now we can be confident that the babel transform is working as expected.

## How to configure for Next.js

In the demo above, we saw how to configure `babel-plugin-transform-imports` in create next app CLI. 

If you are using lodash package with [NextJS](https://nextjs.org/) and if you want to reduce the footprint, it is very simple to do  without doing the extra work of *rewiring* and *customizing* stuff.

Next.js provides a way for us to tweak into babel and webpack configuration without any hassle. This is the thing I like about Nextjs, it sets up all the right defaults for you, and at the same time, it gives you the **root user** permissions to tweak the stuff. Enough said, let us see how to do this (in) next.

First install `babel-plugin-transform-imports`

```bash
npm install --save-dev babel-plugin-transform-imports
```

Then create `.babelrc` file in the root directory and place this code. 

```javascript
{
    "presets": ["next/babel"],
    "plugins": [
        [
            "babel-plugin-transform-imports",
            {
                "lodash": {
                    "transform": "lodash/${member}",
                    "preventFullImport": true
                }
            }
        ]
    ]
}
```

This [custom configuration file](https://nextjs.org/docs/advanced-features/customizing-babel-config) would be picked by next while building your application.

That's it! Congrats 🎉 

Now you have shipped just the code you need to run your application !

> The beauty of the package `babel-plugin-transform-imports` is that it can be used to optimize the imports of other packages as well like Material UI, react-bootstrap etc.

----

References

- [Lodash - Per Method Packages](https://lodash.com/per-method-packages)
- [Material UI - Minimizing Bundle Size](https://material-ui.com/guides/minimizing-bundle-size/)
- [react-app-rewired](https://www.npmjs.com/package/react-app-rewired) 
- [customize-cra](https://www.npmjs.com/package/customize-cra)
- [webpack - Optimization](https://webpack.js.org/configuration/optimization/#optimizationminimize)
- [Analyzing the Bundle Size](https://create-react-app.dev/docs/analyzing-the-bundle-size)
- [Customizing Babel Config in Next.js](https://nextjs.org/docs/advanced-features/customizing-babel-config)