DEV Community

Wojciech Maj
Wojciech Maj

Posted on • Updated on

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!

Top comments (6)

Collapse
 
apoorva_aman profile image
Apoorva Aman

Switching to preactjs.com/

Collapse
 
wojtekmaj profile image
Wojciech Maj

That's a good one! Definitely should be on the list of "hardcore" optimizations.

Collapse
 
apoorva_aman profile image
Apoorva Aman

There's also solidjs.com/
They claim to have better performance

Thread Thread
 
asyncbanana profile image
AsyncBanana

Solid.js does not have any React compatibility like Preact does, but it is still a lot like React, and it is significantly faster than both Preact and React.

Collapse
 
alvechy profile image
Alex Vechy

Why the very sole existance of clsx hurts me so much. classnames was already tiny popular package. Why would someone decide to develop own package instead. Now if you aim to use it (for whatever reason, 200b is literally nothing), your poor devs (current and the future ones) will have to spend some time going to the docs of clsx to check any API differences with classnames. If it's the same API – even more confusing. Why create a new Open Source package when you can contribute to already existing one?

Collapse
 
wojtekmaj profile image
Wojciech Maj

clsx is fully compatible with classnames. Yeah, I don't quite understand what was going on here - sometimes it's maintainers that are attached to their solutions and look down on some contributions fundamentally changing it. Anyways, here's where we are - we have two competing, popular packages doing the same thing and being mutually compatible - so my pick is the one that's both faster and smaller.