Optimizing React apps: Hardcore edition

wojtekmaj

Wojciech Maj

Posted on August 20, 2021

Optimizing React apps: Hardcore edition

You've heard of minifying. You've heard of lazy-loading. You've heard of tree shaking. You've done it all. Or did you? Here are some optimizations you may have never heard of before. Until now!

Enable "loose" transformations in @babel/preset-env

Enabling "loose" transformations may make your application considerably smaller. I shaved off roughly 230.9 KB, or 16.2% from my bundle!

This, however, comes at a price: your application may break, both when enabling, and disabling these transformations.

In my case, the only fix I needed to do was related to iterating over HTMLCollection (document.querySelectorAll(…), document.getElementsByTagName(…) and HTMLFormControlsCollection (form.elements). I was no longer able to do e.g. [...form.elements], I had to swap it out for Array.from(form.elements).

Still tempted by the large savings? Give it a go by enabling loose flag in Babel config:

babel.config.json

  "presets": [
-   "@babel/preset-env"
+   ["@babel/preset-env", {
+     "loose": true
+   }]
  ]
Enter fullscreen mode Exit fullscreen mode

Remove prop-types from your production bundle

PropTypes are incredibly helpful during development, but they are of no use for your users. You can use babel-plugin-transform-react-remove-prop-types to remove PropTypes from your bundle.

To install, run:

npm install --save-dev babel-plugin-transform-react-remove-prop-types
Enter fullscreen mode Exit fullscreen mode

or

yarn add -D babel-plugin-transform-react-remove-prop-types
Enter fullscreen mode Exit fullscreen mode

and add it to your Babel config like so:

babel.config.json

  "env": {
    "production": {
      "plugins": [
+        "transform-react-remove-prop-types"
      ]
    }
  }
Enter fullscreen mode Exit fullscreen mode

Savings will vary depending on the size of your app. In my case, I shaved off 16.5 KB or about 1.2% from my bundle.

Consider unsafe-wrap mode

unsafe-wrap mode is, as the name states, a bit unsafe for the reasons well explained in plugin's docs.

However, in my case, PropTypes were not accessed from anywhere and the application worked flawlessly.

To enable this mode, you need to change your Babel config like so:

babel.config.json

  "env": {
    "production": {
      "plugins": [
-       "transform-react-remove-prop-types"
+       ["transform-react-remove-prop-types", {
+         "mode": "unsafe-wrap"
+       }]
      ]
    }
  }
Enter fullscreen mode Exit fullscreen mode

This way, I shaved off a total of 35.9 KB or about 2.5% from my bundle.

Enable new JSX transform

Enabling new JSX transform will change the way Babel React preset transpiles JSX to pure JavaScript.

I explained the benefits of enabling it in my other article: How to enable new JSX transform in React 17?.

I highly recommend you to have a read. If that's TL;DR though, all you need to do for quick results is make sure that @babel/core and @babel/preset-env in your project are both on version 7.9.0 or newer, and change your Babel config like so:

babel.config.json

  "presets": [
-   "@babel/preset-react"
+   ["@babel/preset-react", {
+     "runtime": "automatic"
+   }]
  ]
Enter fullscreen mode Exit fullscreen mode

And poof! Roughly 10.5 KB, or 0.7% of my bundle was gone.

Minify your HTML

Chances are your bundler is smart enough to minify JavaScript by default when ran in production mode. But did you know you can minify HTML, too? And JavaScript in that HTML as well?

You're in? Great! Here's what you need to do:

Install html-minifier-terser:

npm install --save-dev html-minifier-terser
Enter fullscreen mode Exit fullscreen mode

or

yarn add -D html-minifier-terser
Enter fullscreen mode Exit fullscreen mode

and change your Webpack config to use it. Define minifier options:

webpack.config.js

const minifyOptions = {
  // Defaults used by HtmlWebpackPlugin
  collapseWhitespace: true,
  removeComments: true,
  removeRedundantAttributes: true,
  removeScriptTypeAttributes: true,
  removeStyleLinkTypeAttributes: true,
  useShortDoctype: true,
  // Custom
  minifyCSS: true,
  minifyJS: true,
};
Enter fullscreen mode Exit fullscreen mode

and use them in HtmlWebpackPlugin…:

webpack.config.js

    new HtmlWebpackPlugin({
+     minify: minifyOptions,
      template: 'index.html',
    }),
Enter fullscreen mode Exit fullscreen mode

…as well as in CopyWebpackPlugin:

webpack.config.js

const { minify } = require('html-minifier-terser');
Enter fullscreen mode Exit fullscreen mode

webpack.config.js

  plugins: [
    new CopyWebpackPlugin({
      patterns: [
        {
          from: 'index.html',
          to: '',
+         transform(content) {
+           return minify(content.toString(), minifyOptions);
+         },
        },
      ]
    }),
  ],
Enter fullscreen mode Exit fullscreen mode

Use babel-plugin-styled-components (styled-components users only)

If you use styled-components, make sure to use their Babel plugin, too. Not only it adds minification of styles, but also adds support for server-side rendering, and provides with a nicer debugging experience.

To install, run:

npm install --save-dev babel-plugin-styled-components
Enter fullscreen mode Exit fullscreen mode

or

yarn add -D babel-plugin-styled-components
Enter fullscreen mode Exit fullscreen mode

and add it to your Babel config like so:

babel.config.json

  "env": {
    "production": {
      "plugins": [
+        "styled-components"
      ]
    }
  }
Enter fullscreen mode Exit fullscreen mode

This will shave off a few kilobytes on its own, but due to added displayNames the savings will not be so apparent just yet. So now…

Disable displayName in production builds

babel.config.json

  "env": {
    "production": {
      "plugins": [
+       ["styled-components", {
+         "displayName": false,
+       }]
      ]
    }
  }
Enter fullscreen mode Exit fullscreen mode

Doing so in my app gave me a surprisingly large savings of 50.4 KB or 3.5%.

Wrap createGlobalStyle contents in css (styled-components users only)

Apparently, while babel-plugin-styled-components is capable of minifying styles, it doesn't minify anything within createGlobalStyle. So, chances are you're shipping your app with tons of unnecessary whitespace.

Remove them by simply wrapping createGlobalStyle contents in css as well, like so:

-const GlobalStyle = createGlobalStyle`
+const GlobalStyle = createGlobalStyle`${css`
   // Your global style goes here
-`;
+`}`;
Enter fullscreen mode Exit fullscreen mode

Replace react-lifecycles-compat with an empty mock

react-lifecycles-compat is a dependency that polyfills lifecycle methods introduced in React 16.3 so that the components polyfilled would work with older React versions. Some dependencies may still use this polyfill in order not to break older React version support.

If you use React 16.3 or newer, you don't need react-lifecycles-compat. You can replace it with a mocked version like so:

__mocks__/reactLifecyclesCompatMock.js

module.exports = {
  polyfill: (Component) => Component,
};
Enter fullscreen mode Exit fullscreen mode

webpack.config.js

  resolve: {
    alias: {
+     'react-lifecycles-compat': path.resolve(__dirname, '__mocks__', 'reactLifecyclesCompatMock.js'),
    },
  },
Enter fullscreen mode Exit fullscreen mode

Doing so will save you 2.5 KB.

Replace classnames with clsx

classnames is not a large dependency, only 729 bytes, but clsx is fully compatible with classnames at just 516 bytes. So, replacing classnames with clsx in your app will save you 213 bytes.

Chances are you'll have both classnames and clsx in your app, e.g. because dependencies may require one or the other. In this case, you can use Webpack's alias to get rid of classnames from your bundle:

webpack.config.js

  resolve: {
    alias: {
+     classnames: 'clsx',
    },
  },
Enter fullscreen mode Exit fullscreen mode

Doing so will save you 729 bytes.

Missing something?

Please share your ideas for not-so-obvious optimizations in the comments below!

💖 💪 🙅 🚩
wojtekmaj
Wojciech Maj

Posted on August 20, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Optimizing React apps: Hardcore edition
javascript Optimizing React apps: Hardcore edition

August 20, 2021