gecko-dev/devtools/docs/contributing/react-performance-tips.md

528 строки
20 KiB
Markdown

# 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](./performance.md) 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`](https://reactjs.org/docs/react-component.html)
```jsx
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.
```jsx
function Application(props) {
return <div>{props.content}</div>;
}
```
These functions are equivalent to classes extending `Component`. In
the rest of the article we'll especially focus on the latter. Unless otherwise
stated everything about classes extending `Component` 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](https://reactjs.org/docs/react-api.html#createfactory)
instead:
```javascript
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](https://reactjs.org/docs/react-without-jsx.html).
### The first render
There's only one way to start a React application and trigger a first render:
calling `ReactDOM.render`:
```jsx
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:
1. We call `ReactDOM.render` again with the same component.
```jsx
ReactDOM.render(
<Application content='Good Bye, Cruel World!'/>,
document.getElementById('root')
);
```
2. One component's state changes, through the use of [`setState`](https://reactjs.org/docs/react-component.html#setstate).
If the application is using Redux, this is how Redux-connected components
trigger updates too.
3. 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_](https://reactjs.org/docs/reconciliation.html) 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:
1. triggering the render process **too frequently**,
2. **expensive** render methods,
3. 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](https://reactjs.org/docs/optimizing-performance.html#virtualize-long-lists).
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](shouldcomponentupdate-and-purecomponent-avoiding-renders-altogether).
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](https://reactjs.org/docs/lists-and-keys.html), 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](https://reactjs.org/docs/implementation-notes.html#mounting-as-a-recursive-process)
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](https://reactjs.org/docs/optimizing-performance.html#shouldcomponentupdate-in-action)
is quite complete on this subject.
### Avoiding rerenders with `shouldComponentUpdate`
As the first step of a rerender process, React calls your component's
[`shouldComponentUpdate`](https://reactjs.org/docs/react-component.html#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**.
```jsx
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`](https://reactjs.org/docs/react-api.html#reactpurecomponent):
it will shallowly check the new props and states for reference equality.
```jsx
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 a `Component`
* 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](https://github.com/facebook/immutable-js).
#### 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](https://github.com/kolodny/immutability-helper),
[updeep](https://github.com/substantial/updeep), or even
[Immutable](https://github.com/facebook/immutable-js). 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](#a-few-words-about-redux)) 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:
```javascript
// 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.
```javascript
// 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.
```javascript
// 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**.
```javascript
// 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:
```javascript
// 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:
```javascript
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`](https://redux.js.org/docs/api/combineReducers.html)
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:
```jsx
render() {
return <MyComponent onUpdate={() => this.update()} />;
}
```
Each time the `render` method runs, a new function will be created, and in
`MyComponent`'s `shouldComponentUpdate` 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 new `Set` as it will be
identical.
* Be careful with array's methods, especially `map` or `filter`, 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](https://github.com/reactjs/reselect) is
recommended.
[memoize-immutable](https://github.com/memoize-immutable/memoize-immutable)
can be useful in some cases too.
## Diagnosing performance issues with some tooling
[You can read about it in the dedicated
page](./performance.md#diagnosing-performance-issues-in-react-based-applications).
## 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.