How to Analyze and Improve your 'Create React App' Production Build
Tags: javascript, webdev, react, tutorial
Published: 2021-11-30 14:04:24

If you are interested in a video version of this tutorial, check out the link below. You can follow along with the code in this blog.  _(The video is entirely optional, every step and instruction is covered in the blog post.)_

[![Create React App Analyze Tutorial](https://res.cloudinary.com/dqse2txyi/image/upload/v1638284142/blogs/create-react-app-bundle-analyze/CRA_Youtube_Preview_xaqskf.png)](https://youtu.be/gJxgjyhbGHQ)

In this tutorial we're gonna take a step-by-step look at how to analyze and optimize your _Create React App_ project using a custom webpack configuration that is extremely easy to set up.

We will be using examples that make small adjustments and try different ways of importing modules and splitting your code to see the direct impacts it has on your bundle sizes and performance.  

The goal here is to help you get a better understanding of what _webpack_ is actually doing by seeing the exact changes that occur to a production build when you make small changes to your application.

To begin, we'll create a brand new React app called `something-big-and-bloated`

```bash
npx create-react-app something-big-and-bloated --template typescript
```

Next we'll install the dependencies we need to analyze the project.  

```bash
npm install @craco/craco webpack-bundle-analyzer --save-dev
```

* **craco**: A tool to use a custom webpack configuration with _Create React App_
* **webpack-bundle-analyzer**: A webpack plugin for analyzing bundle size

We need to create a configuration file for `craco` in the root of our project to include our webpack plugin:

`craco.config.js`
```js
const BundleAnalyzerPlugin =
  require("webpack-bundle-analyzer").BundleAnalyzerPlugin;

module.exports = function () {
  return {
    webpack: {
      plugins: [new BundleAnalyzerPlugin({ analyzerMode: "server" })],
    },
  };
};
```

If we run the usual `npm run build` script for a production build, it will use the standard `react-scripts` method.  

However if we run `craco build` it will still run the same process, but inject in any webpack configuration you have included in your `craco.config.js` file.  Pretty sweet.  

Let's give it a try.  We'll create a new entry called **analyze** in `package.json` scripts:

```json
{
  ...
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "analyze": "craco build"
  }
}
```

Now run:

```bash
npm run analyze
```

Because we set the `analyzerMode` to `"server"` in our craco config, we will automatically get our browser open with the results served as a webpage _(you can use the `"json"` option if you want the output without involving the browser)_

![Initial Size](https://res.cloudinary.com/dqse2txyi/image/upload/v1638238643/blogs/create-react-app-bundle-analyze/initial-size_ynrhea.png)

You can hover over each module within the chunks or your can use the little arrow at the upper left which pops out a drawer.  You'll get three different values:

* **stat**: The size of the original source code
* **parsed**: the size of the same code as compiled bundles
* **gzipped**: the size of the compiled bundles after being gzipped

So for most deployments you can look at the gzipped value as how much network usage you will need, and the parsed size as how much code the browser is going to handle once unzipping is complete.  Be aware that this value can be just as problematic on users with less powerful CPUs as the gzip size can be for those on slow networks.  Both are important to consider.

On `create-react-app` v4.0.3 for this tutorial the values I get are 205kb / 135kb / 44kb for stat / parsed / gzipped respectively.  You can see right out of the box we get a bit of overhead (though for most users that's a small price to pay for convenience).  

Now let's try adding some libraries and look at how this value changes.  We'll consider the way that we do our imports and see how we might be able to better control our bundle size by only importing what we need.  

I'm gonna pick a fairly popular UI library called MUI (Material UI).  It's a good example of a large package that can significantly weigh down your app if not bundled properly.  It will make a good example for our tutorial.

We will need the following packages:

* [@mui/material](https://mui.com/) - A React component library designed to give you some useful pre-styled components out of the box
* [@mui/icons-material](https://mui.com/components/material-icons/): An icon package for MUI based on [material icons](https://fonts.google.com/icons)
* [@emotion/react](https://www.npmjs.com/package/@emotion/react): Required for `@mui/material`.
* [@emotion/styled](https://www.npmjs.com/package/@emotion/styled): Required for `@mui/material`.


```bash
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled --save
```

Before we even do anything let's run our analyzer again.  Remember we've added these libraries, but we aren't actually _using_ any of them yet.  Do you think our bundle size will increase?  Let's find out:

```bash
npm run analyze
```

205kb / 135kb / 44kb again.  Exact same result.  That's great!  That means that webpack is not including any libraries we don't actually use.  It's doing its job well.

Now we'll import a component from MUI.  We'll grab a relatively complex one, something more than a button.  Let's use the [Speed Dial](https://mui.com/components/speed-dial/)!  Create a new component file in `src`:

`src/CustomSpeedDial.tsx`
```tsx
import React from "react";
import Box from "@mui/material/Box";
import SpeedDial from "@mui/material/SpeedDial";
import SpeedDialIcon from "@mui/material/SpeedDialIcon";
import SpeedDialAction from "@mui/material/SpeedDialAction";
import FileCopyIcon from "@mui/icons-material/FileCopyOutlined";
import SaveIcon from "@mui/icons-material/Save";
import PrintIcon from "@mui/icons-material/Print";
import ShareIcon from "@mui/icons-material/Share";

const actions = [
  { icon: <FileCopyIcon />, name: "Copy" },
  { icon: <SaveIcon />, name: "Save" },
  { icon: <PrintIcon />, name: "Print" },
  { icon: <ShareIcon />, name: "Share" },
];

export default function CustomSpeedDial() {
  return (
    <Box sx={{ height: 320, transform: "translateZ(0px)", flexGrow: 1 }}>
      <SpeedDial
        ariaLabel="SpeedDial basic example"
        sx={{ position: "absolute", bottom: 16, left: 16 }}
        icon={<SpeedDialIcon />}
      >
        {actions.map((action) => (
          <SpeedDialAction
            key={action.name}
            icon={action.icon}
            tooltipTitle={action.name}
          />
        ))}
    </Box>
  );
}
```

Replace the contents of your `App.tsx` file with the following:

`src/App.tsx`
```tsx
import React from "react";
import CustomSpeedDial from "./CustomSpeedDial";

function App() {
  return <CustomSpeedDial />;
}

export default App;
```

Run the development server to check it out:

```bash
npm run start
```

![MUI Speed Dial](https://res.cloudinary.com/dqse2txyi/image/upload/v1638242651/blogs/create-react-app-bundle-analyze/speeddial_ugf7ue.png)

Everything looks good.  Let's see how much that impacted our build.  Run our analyze command again:

```bash
npm run analyze
```

![BUndle With Speed Dial](https://res.cloudinary.com/dqse2txyi/image/upload/v1638242899/blogs/create-react-app-bundle-analyze/mui-size_dntazk.png)

Our bundle size is now up to 660kb / 270kb / 88kb.  That's a significant increase for one component!  Of course bear in mind that it's fairly complex, and as soon as you use one you need to include all the other dependencies that make MUI function.  

I'd bet if you added a second component you wouldn't get such a big jump.  In fact we can try now.  Add the following to your SpeedDial component:

`src/CustomSpeedDial.tsx`:
```tsx
import React from "react";
import Box from "@mui/material/Box";
import SpeedDial from "@mui/material/SpeedDial";
import SpeedDialIcon from "@mui/material/SpeedDialIcon";
import SpeedDialAction from "@mui/material/SpeedDialAction";
import FileCopyIcon from "@mui/icons-material/FileCopyOutlined";
import SaveIcon from "@mui/icons-material/Save";
import PrintIcon from "@mui/icons-material/Print";
import ShareIcon from "@mui/icons-material/Share";

// NEW
import Button from "@mui/material/Button";

const actions = [
  { icon: <FileCopyIcon />, name: "Copy" },
  { icon: <SaveIcon />, name: "Save" },
  { icon: <PrintIcon />, name: "Print" },
  { icon: <ShareIcon />, name: "Share" },
];

export default function CustomSpeedDial() {
  return (
    <Box sx={{ height: 320, transform: "translateZ(0px)", flexGrow: 1 }}>
      {/* NEW */}
      <Button variant="contained">Hello world!</Button>
      <SpeedDial
        ariaLabel="SpeedDial basic example"
        sx={{ position: "absolute", bottom: 16, left: 16 }}
        icon={<SpeedDialIcon />}
      >
        {actions.map((action) => (
          <SpeedDialAction
            key={action.name}
            icon={action.icon}
            tooltipTitle={action.name}
          />
        ))}
      </SpeedDial>
    </Box>
  );
}
```

We've imported a button above and included it with our speed dial _(two new lines marked with "NEW" comments.)_

When we run `npm run analyze` again we get.... almost the same!  677kb / 278kb / 89kb.  We can see that the button extremely little to the bundle size since most of the building blocks for it were already included with the Speed Dial.  

But now let's compare when using the traditional commonJS import.  

Add the following line to the very top of your `CustomSpeedDial` component _(if ESLint complains about import order, place the line after all your imports statements)_

`src/CustomSpeedDial.tsx`
```tsx
const material = require("@mui/material");
```

Analyze again:

```bash
npm run analyze
```

![MUI Everything Bundled](https://res.cloudinary.com/dqse2txyi/image/upload/v1638243852/blogs/create-react-app-bundle-analyze/mui-everything_vgh01j.png)

Holy moly!  1.97**MB** / 697kb / 194kb.  

What happened?  Looks like we're bundling the **entire** MUI library.  Popper?  Tooltip.js?  We're not using any of those, but they're taking up a ton of space in our chunk.

It turns out that when we are using ES6 modules webpack is very good at figuring out which pieces of code we are actually using based on what we import and export.  

This process is called [tree shaking](https://webpack.js.org/guides/tree-shaking/) and it requires you to use ES6 modules in order to work.  You can see that doing so can have a very dramatic effect on our final bundle.  

Our current program is exactly the same functionally as our previous one, but thanks to a single commonJS import it's a whopping 3x the size.  Yikes!

Here's what we're going to do though.  Instead of removing that `require` in our `CustomSpeedDial` we're going to leave it there, and introduce something called [code spltting](https://create-react-app.dev/docs/code-splitting/) as another option available to you.

**Code splitting** is effective when you have a certain component or page or general part of your application that isn't necessary required for every visitor.  It might be a datepicker that only appears when a user makes a booking, or it might be a "help" page that only a small percentage of users will need.

We can use React's [lazy and suspense](https://reactjs.org/docs/code-splitting.html) features to break these pieces into separate bundle chunks, and only load them when necessary.  

Let's update `App.tsx`.  There's a lot to unpack here, so we'll just show the code first and break it down:

`src/App.tsx`
```tsx
import CircularProgress from "@mui/material/CircularProgress";
import Button from "@mui/material/Button";
import React, { Suspense, useState } from "react";

// 1
const CustomSpeedDial = React.lazy(() => import("./CustomSpeedDial"));

function App() {
  // 2
  const [showSpeedDial, setShowSpeedDial] = useState(false);

  // 4
  if (showSpeedDial) {
    return (
      // 5
      <Suspense fallback={<CircularProgress />}>
        <CustomSpeedDial />
      </Suspense>
    );
  }

  return (
    // 3
    <Button variant="contained" onClick={() => setShowSpeedDial(true)}>
      Click to load speed dial
    </Button>
  );
}

export default App;
```

The numbers are a little out of order, but that's intentional.  You'll see it follows the actual flow of the component.

1. We "lazy" import the `CustomSpeedDial` module.  Remember that's the one that uses `require` for the entire MUI package and comes in at 1-2MB.  By using the lazy import, what happens is that the import only occurs when our primary component here (CustomSpeedDial) actually tries to render it.  We'll see that it doesn't by default.  
2. A boolean React state value to keep track of whether which component we want to render.  The default `false` value means we will not be rendering `CustomSpeedDial`
3.  Our default component is a basic `Button` imported directly from MUI.  When this button is pressed it sets the value of `showSpeedDial` to _true_.
4. Once `showSpeedDial` is _true_ we take this branch at the next render.  The reason we get a re-render is because we  updated a stateful React value (showSpeedDial).
5. The purpose of the `Suspense` component is to tells React what to render while waiting for the module to import.  Depending on the size it could take a second or more.  For our example we are using MUI's `CircularProgress` to imply a loading state while the module is loading.  Once it loads it switches to render the children of the Suspense component.  

Now it's time to try it out!  We'll begin with an _analyze_:

```bash
npm run analyze
```
![MUI Code Splitting](https://res.cloudinary.com/dqse2txyi/image/upload/v1638248309/blogs/create-react-app-bundle-analyze/mui-code-splitting_u7lju6.png)

This gets really interesting.  Webpack has created new separate chunks.  You'll notice there are more `.js` chunks when you toggle the drawer on the left.



In fact this largest chunk on the left `3.5d1a4e88.chunk.js` (1.52mb / 475kb / 122kb) isn't even used on the default load of our app.  Based on our learnings earlier, we can see that huge chunk is definitely our `CustomSpeedDial.tsx` component that imports all of MUI with `require` commonJS import.  

On the right we have the much smaller bundle `2.c5828938.chunk.js` that includes things like `Button` and `ButtonBase`.  This is the chunk that will load on every page load.  We can look at the size (451kb / 214kb / 69kb) and validate that in a moment.  

Since our ultimate goal is to make sure our production app is running as efficiently as possible, we want to run our tests on the production version of the app.  Use the following command to build a production version of the app:

```bash
bpm run build
```

Next we'll need to serve that `build` directory that was created.  If you have your own local serve you prefer to use, use it!  If not just add the [serve](https://www.npmjs.com/package/serve) package:

```bash
npm install serve --save-dev
```

And then use it to serve the `build` directory:

```bash
npx serve build
```

You can find the page at [http://localhost:3000/]() _(or whatever port serve specifies on the command line)_.

Press F12 to open your browser's _Developer Tools_ and click the _Network_ tab.  This process should be roughly the same in both Chrome, Edge and Firefox.

I am using Firefox so the screenshots should match your experience.  If you are using another browser the options will still be there, just potentially in a different location.

Click the _Disable Cache_ checkbox so that we are loading the JS files on every refresh and not a cached version from the browser.  We want to be able to see the load times and sizes.

Now hit the refresh button (F5).

![Code Splitting First Load](https://res.cloudinary.com/dqse2txyi/image/upload/v1638249023/blogs/create-react-app-bundle-analyze/code-split-first-load_d7rat4.png)

As we predicted, our total transfer is 82KB with 69KB of that being the highlighted smaller `c5828938` chunk we identified _(remember this is a served production build, so we are working with GZIP sizes just like your real app would for real users)_ 

There is no sign of the 122KB gzipped chunk.  Let's click the "load speed dial" button on our app.

![Code SPlitting Second Load](https://res.cloudinary.com/dqse2txyi/image/upload/v1638249424/blogs/create-react-app-bundle-analyze/code-split-second-load_nt4mlo.png)

There's that 122KB chunk with our `CustomSpeedDial` component inside of it.  

How cool is it that it only loaded the code on demand?

## Wrapping Up

I hope you can start brainstorming ways to reduce the bundle size of your app and potentially introduce code splitting to improve initial load times.  

Also worth noting: these tips are not exclusive to _Create React App_.  All we did was introduce a special tool called `craco` to allow us to configure webpack.  Any application running webpack can potentially benefit from these tips!

It would be irresponsible to me not to mention that _Create React App_ does recommend a similar tool that doesn't require `craco` _(although personally I feel it's not as intuitive to read the data)_ it still gets the job done well.  Read about it [here](https://create-react-app.dev/docs/analyzing-the-bundle-size/).

## Continue Learning

Please check some of my other learning tutorials.  Feel free to leave a comment or question and share with others if you find any of them helpful:

- [Learnings from React Conf 2021](https://dev.to/alexeagleson/learnings-from-react-conf-2021-17lg)

- [How to Create a Dark Mode Component in React](https://dev.to/alexeagleson/how-to-create-a-dark-mode-component-in-react-3ibg)

- [How to Analyze and Improve your 'Create React App' Production Build ](https://dev.to/alexeagleson/how-to-analyze-and-improve-your-create-react-app-production-build-4f34)

- [How to Create and Publish a React Component Library](https://dev.to/alexeagleson/how-to-create-and-publish-a-react-component-library-2oe)

- [How to use IndexedDB to Store Local Data for your Web App ](https://dev.to/alexeagleson/how-to-use-indexeddb-to-store-data-for-your-web-application-in-the-browser-1o90)

- [Running a Local Web Server](https://dev.to/alexeagleson/understanding-the-modern-web-stack-running-a-local-web-server-4d8g)

- [ESLint](https://dev.to/alexeagleson/understanding-the-modern-web-stack-linters-eslint-59pm)

- [Prettier](https://dev.to/alexeagleson/understanding-the-modern-web-stack-prettier-214j)

- [Babel](https://dev.to/alexeagleson/building-a-modern-web-stack-babel-3hfp)

- [React & JSX](https://dev.to/alexeagleson/understanding-the-modern-web-stack-react-with-and-without-jsx-31c7)

- [Webpack: The Basics](https://dev.to/alexeagleson/understanding-the-modern-web-stack-webpack-part-1-2mn1)

- [Webpack: Loaders, Optimizations & Bundle Analysis](https://dev.to/alexeagleson/understanding-the-modern-web-stack-webpack-part-2-49bj)

---

For more tutorials like this, follow me <a href="https://twitter.com/eagleson_alex?ref_src=twsrc%5Etfw" class="twitter-follow-button" data-show-count="false">@eagleson_alex</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> on Twitter