20 KiB
Writing efficient React code
In this article we'll discuss about the various component types we can use, as well as discuss some tips to make your React application faster.
TL;DR tips
- Prefer props and state immutability and use
PureComponent
components as a default - As a convention, the object reference should change if and only if the inner data
changes.
- Be careful to never use new instance of functions as props to a Component (it's fine to use them as props to a DOM element).
- Be careful to not update a reference if the inner data doesn't change.
- Always measure before optimizing to have a real impact on performance. And always measure after optimizing too, to prove your change had a real impact.
How React renders normal components
What's a normal component?
As a start let's discuss about how React renders normal plain components, that
don't use shouldComponentUpdate
. What we call plain components here are either:
- classes that extend
Component
class Application extends React.Component { render() { return <div>{this.props.content}</div>; } }
- normal functions that take some
props
as parameter and return some JSX. We call these functions either Stateless Components or Functional Components. This is important to understand that these Stateless Components are not especially optimized in React.
These functions are equivalent to classes extendingfunction Application(props) { return <div>{props.content}</div>; }
Component
. In the rest of the article we'll especially focus on the latter. Unless otherwise stated everything about classes extendingComponent
is also true for Stateless/Functional Components.
Notes on the use of JSX
Because we don't use a build step in mozilla-central yet, some of our tools don't use JSX and use factories instead:
class Application extends React.Component {
render() {
return dom.div(null, this.props.content);
}
}
We'll use JSX in this documentation for more clarity but this is strictly equivalent. You can read more on React documentation.
The first render
There's only one way to start a React application and trigger a first render:
calling ReactDOM.render
:
ReactDOM.render(
<Application content='Hello World!'/>,
document.getElementById('root')
);
React will call that component's render
method, and then recursively call
every child's render
method, generating a rendering tree and then a virtual
DOM tree. It will then render actual DOM elements to the specified container.
Subsequent rerenders
There are several ways to trigger a rerender:
- We call
ReactDOM.render
again with the same component.
ReactDOM.render(
<Application content='Good Bye, Cruel World!'/>,
document.getElementById('root')
);
- One component's state changes, through the use of
setState
. If the application is using Redux, this is how Redux-connected components trigger updates too. - One component's props change. But note that this can't happen by itself, this is always a consequence of the case 1 or 2 in one of its parents. So we'll ignore this case for this chapter.
When one of these happens, just like the initial render, React will call that
component's render
method, and then recursively call every child's render
method, but this time possibly with changed props compared to the previous render.
These recursive calls produce a new rendering tree. That's where React uses an algorithm called virtual diffing or reconciliation to find the minimal set of updates to apply to the DOM. This is good because the less updates to the DOM the less work the browser has to do to reflow and repaint the application.
Main sources of performance issues
From this explanation we can gather that the main performance issues can come from:
- triggering the render process too frequently,
- expensive render methods,
- the reconciliation algorithm itself. The algorithm is O(n) according to React authors, which means the processing duration increases linearly with the number of elements in the tree we compare. So a larger tree means a longer time to process.
Let's dive more into each one of these issues.
Do not render too often
A rerender will happen after calling setState
to change the
local state.
Everything that's in the state should be used in render
.
Anything in the state that's not used in render
shouldn't be in the state, but
rather in an instance variable. This way you won't trigger an update if you
change some internal state that you don't want to reflect in the UI.
If you call setState
from an event handler you may call it too often.
This is usually not a problem because React is smart enough to merge close
setState calls and trigger a rerender only once per frame. Yet if your render
is expensive (see below as well) this could lead to problems and you may want to
use setTimeout
or other similar techniques to throttle the renders.
Keep render
methods as lean as possible
When rendering a list, it's very common that we'll map this list to a list of components. This can be costly and we might want to cut this list in several chunks of items or to virtualize this list. Although this is not always possible or easy.
Do not do heavy computations in your render
methods. Rather do them before
setting the state, and set the state to the result of these computations.
Ideally render
should be a direct mirror of the component's props and state.
Note that this rule also applies to the other methods called as part of the
rendering process: componentWillUpdate
and componentDidUpdate
. In
componentDidUpdate
especially avoid synchronous reflows by getting DOM
measurements, and do not call setState
as this would trigger yet another
update.
Help the reconciliation algorithm be efficient
The smaller the tree is, the faster the algorithm is. So it's
useful to limit the changes to a subtree of the full tree. Note that the use of
shouldComponentUpdate
or PureComponent
alleviates this issue by cutting off
entire branches from the rendering tree, we discuss this in more details
below.
Try to change the state as close as possible to where your UI should change (close in the components tree).
Do not forget to set key
attributes when rendering a list of
things, which shouldn't be the
array's indices but something that identifies the item in a predictable, unique
and stable way. This helps the algorithm
a lot by skipping parts that likely haven't changed.
More documentation
The React documentation has a very well documented page explaining the whole render and rerender process.
shouldComponentUpdate
and PureComponent
: avoiding renders altogether
React has an optimized algorithm to apply changes. But the fastest algorithm is an algorithm that isn't executed at all.
React's own documentation about performance is quite complete on this subject.
Avoiding rerenders with shouldComponentUpdate
As the first step of a rerender process, React calls your component's
shouldComponentUpdate
method with 2 parameters: the new props, and the new
state. If this method returns false, then React will skip the render process for this
component, and its whole subtree.
class ComplexPanel extends React.Component {
// Note: this syntax, new but supported by Babel, automatically binds the
// method with the object instance.
onClick = () => {
this.setState({ detailsOpen: true });
}
// Return false to avoid a render
shouldComponentUpdate(nextProps, nextState) {
// Note: this works only if `summary` and `content` are primitive data
// (eg: string, number) or immutable data
// (keep reading to know more about this)
return nextProps.summary !== this.props.summary
|| nextProps.content !== this.props.content
|| nextState.detailsOpen !== this.state.detailsOpen;
}
render() {
return (
<div>
<ComplexSummary summary={this.props.summary} onClick={this.onClick}/>
{this.state.detailsOpen
? <ComplexContent content={this.props.content} />
: null}
</div>
);
}
}
This is a very efficient way to improve your application speed, because this avoids everything: both calling render methods for this component and the whole subtree, and the reconciliation phase for this subtree.
Note that just like the render
method, shouldComponentUpdate
is called once
per render cycle, so it needs to be very lean and return as fast as possible. So
it should execute some cheap comparisons only.
PureComponent
and immutability
A very common implementation of shouldComponentUpdate
is provided by React's
PureComponent
:
it will shallowly check the new props and states for reference equality.
class ComplexPanel extends React.PureComponent {
// Note: this syntax, new but supported by Babel, automatically binds the
// method with the object instance.
onClick = () => {
// Running this repeatidly won't render more than once.
this.setState({ detailsOpen: true });
}
render() {
return (
<div>
<ComplexSummary summary={this.props.summary} onClick={this.onClick}/>
{this.state.detailsOpen
? <ComplexContent content={this.props.content} />
: null}
</div>
);
}
}
This has a very important consequence: for non-primitive props and states, that is
objects and arrays that can be mutated without changing the reference itself,
PureComponent's inherited shouldComponentUpdate
will yield wrong results and will
skip renders where it shouldn't.
So you're left with one of these two options:
- either implement your own
shouldComponentUpdate
in aComponent
- or (preferred) decide to make all your data structure immutable.
The latter is recommended because:
- It's much simpler to think about.
- It's much faster to check for equality in
shouldComponentUpdate
and in other places (like Redux' selectors).
Note you could technically implement your own shouldComponentUpdate
in a
PureComponent
but this is quite useless because PureComponent
is nothing
more than Component
with a default implementation for shouldComponentUpdate
.
About immutability
What it doesn't mean
It doesn't mean you need to enforce the immutability using a library like Immutable.
What it means
It means that once a structure exists, you don't mutate it.
Every time some data changes, the object reference must change as well. This means a new object or a new array needs to be created. This gives the nice reverse guarantee: if the object reference has changed, the data has changed.
It's good to go one step further to get a strict equivalence: if the data doesn't change, the object reference mustn't change. This isn't necessary for your app to work, but this is a lot better for performance as this avoids spurious rerenders.
Keep reading to learn how to proceed.
Keep your state objects simple
Updating your immutable state objects can be difficult if the objects used are complex. That's why it's a good idea to keep the objects simple, especially keep them not nested, so that you don't need to use a library like immutability-helper, updeep, or even Immutable. Be especially careful with Immutable as it's easy to create performance problems by misusing its API.
If you're using Redux (see below as well) this advice applies to your individual reducers as well, even if Redux tools make it easy to have a nested/combined state.
How to update an object
Updating an object is quite easy.
You must not change/add/delete inner properties directly:
// Note that in the following examples we use the callback version
// of `setState` everywhere, because we build the new state from
// the current state.
// Please don't do this as this will likely induce bugs.
this.setState(state => {
state.stateObject.details = details;
return state;
});
// This is wrong too: `stateObject` is still mutated.
this.setState(({ stateObject }) => {
stateObject.details = details;
return { stateObject };
});
Instead you must create a new object for this property. In this example we'll use the object spread operator, already implemented in Firefox, Chrome and Babel.
However here we take care to return the same object if it doesn't need an update. The comparison happens inside the callback because it depends on the state as well. This is a good thing to do so that the shallow equality check doesn't return false if nothing changes.
// Updating one property in the state
this.setState(({ stateObject }) => ({
stateObject: stateObject.content === newContent
? stateObject
: { ...stateObject, content: newContent },
});
// This is very similar if 2 properties need an update:
this.setState(({ stateObject1, stateObject2 }) => ({
stateObject1: stateObject1.content === newContent
? stateObject1
: { ...stateObject1, content: newContent },
stateObject2: stateObject2.details === newDetails
? stateObject2
: { ...stateObject2, details: newDetails },
});
// Or if one of the properties needs to update 2 of it's own properties:
this.setState(({ stateObject }) => ({
stateObject: stateObject.content === newContent && stateObject.details === newDetails
? stateObject
: { ...stateObject, content: newContent, details: newDetails },
});
Note that this isn't about the returned state
object, but its properties.
The returned object is always merged into the current state, and React creates
a new component's state object at each update cycle.
How to update an array
Updating an array is easy too.
You must avoid methods that mutate the array like push/splice/pop/shift and you must not change directly an item.
// Please don't do this as this will likely induce bugs.
this.setState(({ stateArray }) => {
stateArray.push(newItem); // This is wrong
stateArray[1] = newItem; // This is wrong too
return { stateArray };
});
Instead here again you need to create a new array instance.
// Adding an element is easy.
this.setState(({ stateArray }) => ({
stateArray: [...stateArray, newElement],
}));
this.setState(({ stateArray }) => {
// Removing an element is more involved.
const newArray = stateArray.filter(element => element !== removeElement);
// or
const newArray = [...stateArray.slice(0, index), ...stateArray.slice(index + 1)];
// or do what you want on a new clone:
const newArray = stateArray.slice();
return {
// Because we want to keep the old array if removeElement isn't in the
// filtered array, we compare the lengths.
// We still start a render phase because we call `setState`, but thanks to
// PureComponent's shouldComponentUpdate implementation we won't actually render.
stateArray: newArray.length === stateArray.length ? stateArray : newArray,
};
// You can also return a falsy value to avoid the render cycle at all:
return newArray.length === stateArray.length
? null
: { stateArray: newArray };
});
How to update Maps and Sets
The process is very similar for Maps and Sets. Here is a quick example:
// For a Set
this.setState(({ stateSet }) => {
if (!stateSet.has(value)) {
stateSet = new Set(stateSet);
stateSet.add(value);
}
return { stateSet };
});
// For a Map
this.setState(({ stateMap }) => {
if (stateMap.get(key) !== value) {
stateMap = new Map(stateMap);
stateMap.set(key, value);
}
return { stateMap };
}));
How to update primitive values
Obviously, with primitive types like boolean, number or string, that are
comparable with the operator ===
, it's much easier:
this.setState({
stateString: "new string",
stateNumber: 42,
stateBool: false,
});
Note that we don't use the callback version of setState
here. That's because
for primitive values we don't need to use the previous state to generate a new
state.
A few words about Redux
When working with Redux, the rules stay the same, except all of this
happens in your reducers instead of in your components. With Redux comes the
function combineReducers
that obeys all the rules we outlined before while making it possible to have a
nested state.
shouldComponentUpdate
or PureComponent
?
It is highly recommended to go the full PureComponent + immutability route,
instead of writing custom shouldComponentUpdate
implementations for
components. This is more generic, more maintainable, less error-prone, faster.
Of course all rules have exceptions and you're free to implement a
shouldComponentUpdate
method if you have specific cases to take care of.
Some gotchas with PureComponent
Because PureComponent
shallowly checks props and state, you need to take care
to not create a new reference for something that's otherwise identical. Some
common cases are:
-
Using a new instance for a prop at each render cycle. Especially, do not use a bound function or an anonymous function (both classic functions or arrow functions) as a prop:
render() { return <MyComponent onUpdate={() => this.update()} />; }
Each time the
render
method runs, a new function will be created, and inMyComponent
'sshouldComponentUpdate
the shallow check will always fail defeating its purpose. -
Using another reference for the same data. One very common example is the empty array: if you use a new
[]
for each render, you won't skip render. A solution is to reuse a common instance. Be careful as this can very well be hidden within some complicated Redux reducers. -
A similar issue can arise if you use sets or maps. If you add an element in a
Set
that's already in there, you don't need to return a newSet
as it will be identical. -
Be careful with array's methods, especially
map
orfilter
, as they always return a new array. So even with the same inputs (same input array, same function), you'll get a new output, even if it contains the same data. If you're using Redux, reselect is recommended. memoize-immutable can be useful in some cases too.
Diagnosing performance issues with some tooling
You can read about it in the dedicated page.
Breaking the rules: always measure first
You should generally follow these rules because they bring a consistent performance in most cases.
However you may have specific cases that will need that you break the rules. In that case the first thing to do is to measure using a profiler so that you know where your problem are.
Then and only then you can decide to break the rules by using some mutable state
and/or custom shouldComponentUpdate
implementation.
And remember to measure again after you did your changes, to check and prove that your changes actually made an impact. Ideally you should always give links to profiles when requesting a review for a performance patch.