зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1435991 - Document React component best usage r=gregtatum,nchevobbe,ochameau,yulia
MozReview-Commit-ID: CaaVjxe5CsK --HG-- extra : rebase_source : 63e6bcc3f3bc01ba271470311e797850a29f275d
This commit is contained in:
Родитель
d40743523c
Коммит
c9ff88fb36
|
@ -15,6 +15,7 @@
|
|||
* [Code reviews](./contributing/code-reviews.md)
|
||||
* [Filing good bugs](./contributing/filing-good-bugs.md)
|
||||
* [Investigating performance issues](./contributing/performance.md)
|
||||
* [Writing efficient React code](./contributing/react-performance-tips.md)
|
||||
* [Automated tests](tests/README.md)
|
||||
* Running tests
|
||||
* [`xpcshell`](tests/xpcshell.md)
|
||||
|
|
|
@ -0,0 +1,527 @@
|
|||
# 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.
|
||||
|
||||
**Everytime 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.
|
Загрузка…
Ссылка в новой задаче