Optimizing React apps: Hardcore edition
Wojciech Maj
Posted on August 20, 2021
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
+ }]
]
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
or
yarn add -D babel-plugin-transform-react-remove-prop-types
and add it to your Babel config like so:
babel.config.json
"env": {
"production": {
"plugins": [
+ "transform-react-remove-prop-types"
]
}
}
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"
+ }]
]
}
}
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"
+ }]
]
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
or
yarn add -D html-minifier-terser
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,
};
and use them in HtmlWebpackPlugin
…:
webpack.config.js
new HtmlWebpackPlugin({
+ minify: minifyOptions,
template: 'index.html',
}),
…as well as in CopyWebpackPlugin
:
webpack.config.js
const { minify } = require('html-minifier-terser');
webpack.config.js
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: 'index.html',
to: '',
+ transform(content) {
+ return minify(content.toString(), minifyOptions);
+ },
},
]
}),
],
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
or
yarn add -D babel-plugin-styled-components
and add it to your Babel config like so:
babel.config.json
"env": {
"production": {
"plugins": [
+ "styled-components"
]
}
}
This will shave off a few kilobytes on its own, but due to added displayName
s 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,
+ }]
]
}
}
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
-`;
+`}`;
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,
};
webpack.config.js
resolve: {
alias: {
+ 'react-lifecycles-compat': path.resolve(__dirname, '__mocks__', 'reactLifecyclesCompatMock.js'),
},
},
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',
},
},
Doing so will save you 729 bytes.
Missing something?
Please share your ideas for not-so-obvious optimizations in the comments below!
Posted on August 20, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.