Improve React performance with Babel

by Olivier Tassinari

React is well known for its virtual DOM implementation and its good performance out of the box.

There are many ways to speed it up. Just to name a few:

  • You can use process.env.node_env = ‘production’ to disable all the checks that React is doing in the development environment. For instance, that’s going to bypass the propTypes validation.
  • You can prune the reconciliation tree with the shouldComponentUpdate lifecycle method. The depth of the pruned node matters a lot. The lower the depth, the better.
  • You can smartly use the key property on a long list of elements. The idea is to give useful information to React so it can identify each element and perform as few DOM mutations as possible.

These three approaches are very efficient. You will sooner or later have to use them as your application grows and you want to give users the best possible experience. Sometimes, these speedup tips are not enough and you start to investigate ways to push performance further.

Babel, a powerful tool

You may already know Babel. It has gotten famous by allowing us to use ES6 in production with browsers that are not supporting it yet.

It used to be called 6to5, but with version 6, Babel has become much more than that. It’s now a powerful tool to apply code transforms at the AST level. You can do such things with other tools but this one is pretty convenient. People may not realize it, but they are already using a lot of AST transformation functions when they write ES6 and JSX code. Those ES6 and JSX transformation functions are packaged under two presets:

Things started to be really interesting when Facebook released React 0.14.0. They have introduced two compiler optimizations that we can enable in production. We hadn’t heard anything about it on Twitter nor Medium. We have realized it by carefully reading the release note:

React now supports two compiler optimizations that can be enabled in Babel 5.8.24 and newer. Both of these transforms should be enabled only in production (e.g., just before minifying your code) because although they improve runtime performance, they make warning messages more cryptic and skip important checks that happen in development mode, including propTypes.

Constant hoisting for React elements

The first compiler optimization proposed by Facebook hoists the creation of elements that are fully static to the top level. A component is fully static or referentially transparent when it can be replaced with its value without changing the behavior.

It’s a Babel plugin called transform-react-constant-elements. Let’s have a look at how it behaves with a simple example:

In

const Hr = () => {
return <hr className="hr" />;
}

Out

const _ref = <hr className="hr" />;
const Hr = () => {
return _ref;
};

This transform has two advantages:

  • It tells React that the subtree hasn’t changed so React can completely skip it when reconciling.
  • It reduces calls to React.createElement and the resulting memory allocations.

However, there are some limitations that you should be aware of. It has two documented deoptimizations. It won’t work if you are using the ref property or if you are spreading properties.

Inlining React elements

The second compiler optimization proposed by Facebook is replacing the React.createElement function with a more optimized one for production: babelHelpers.jsx.

It’s a Babel plugin called transform-react-inline-elements. Let’s have a look at how it behaves with a simple example:

In

<Baz foo="bar" key="1" />;

Out

babelHelpers.jsx(Baz, {
foo: 'bar'
}, '1');
/**
* Instead of
*
* React.createElement(Baz, {
* foo: 'bar',
* key: '1',
* });
*/

The advantage of this transform is skipping a loop through props. The babelHelpers.jsx method has a slightly different API than the React.createElement. It accepts a props argument that skips this specific loop:

// Remaining properties are added to a new props object
for (propName in config) {
if (config.hasOwnProperty(propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName];
}
}

However, there are some limitations that you should be aware of. It has two documented deoptimizations, the same than before. It won’t work if you are using the ref property or if you are spreading properties.
If you are targeting older browser and not parsing the node_modules with Babel, you need a global polyfill for ES6 Symbol.

I have also noticed one issue. You can’t use the JSX syntax like this:

<Navbar.Header />

Remove propTypes

Looking at the power of the previous transforms, we have soon been wondering if we couldn’t get rid of propTypes in production. As soon as we set process.env.node_env = ‘production’ all propTypes definitions are simply dead code.

I had found a great plugin written by Nikita Gusakov for this specific use case. Unfortunately, at that time, it was only working with Babel 5. I ended up forking it with transform-react-remove-prop-types. Let’s have a look at how it behaves with a simple example:

In

const Baz = () => (
<div />
);
Baz.propTypes = {
foo: React.PropTypes.string
};

Out

const Baz = () => (
<div />
);

The advantage of this transform is saving bandwidth. We are not aware of any limitation.

Let’s benchmark this!

Applying plugins in production without ways to measure their impact is like shooting in the dark. You shouldn’t do it.

How efficient are those optimizations? At Doctolib, we were wondering the same thing. We have built a complex calendar that can display thousands of appointments with doctors on a single view. We want the interface to be as fast as possible so that doctors can focus on what matters.

Anonymized calendar

Thankfully, some tools are available.

react-addons-perf

The first tool that we can use to benchmark is an addon built by Facebook: react-addons-perf. It’s pretty convenient to measure wasted time by component. In our case, we are going to focus on the inclusive time. The protocol is the following. We are rendering 50 times the calendar component with a fair amount of events (500). In the meantime, react-addons-perf is measuring the inclusive time spent. Here are the results (the lower the better):

Notice that we can significantly improve the runtime performance with the three previous transforms. The runtime win is around 25% without significant build size impact.

However, we can’t rely on this benchmark as the addon requires process.env.node_env != ‘production’ to work. We need a way to benchmark the rendering with process.env.node_env = ‘production’.

benchmark.js

The second tool that we have used is React agnostic: benchmark.js. The protocol is pretty much the same. We are mounting and unmounting the calendar component 100 times. Here are the results:

Notice that the very first step benchmarked is setting process.env.node_env = ‘production’ and that the runtime unit changed. We are measuring operation per second, the higher, the better.
The runtime win is 7.24% ± 0.45% with those three previous transforms. That’s less impressive but still interesting as enabling these transforms is cheap.

You can check out the code for this benchmarks.

To wrap up

We highly advise you to consider adding those different compiler optimizations. We have already started spreading the word to the community: spectacle, react-redux-starter-kit, react-starter-kit, react-redux-universal-hot-example.

But keep in mind that these transforms should only be enabled in production. They make warning messages more cryptic and skip important checks that happen in development mode, like propTypes.