diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/docs/contributor/contributing/react-performance-tips.md | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/docs/contributor/contributing/react-performance-tips.md')
-rw-r--r-- | devtools/docs/contributor/contributing/react-performance-tips.md | 527 |
1 files changed, 527 insertions, 0 deletions
diff --git a/devtools/docs/contributor/contributing/react-performance-tips.md b/devtools/docs/contributor/contributing/react-performance-tips.md new file mode 100644 index 0000000000..edf0712d88 --- /dev/null +++ b/devtools/docs/contributor/contributing/react-performance-tips.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) + ```javascript + 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. + ```javascript + 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`: + +```javascript +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. + ```javascript + 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**. + +```javascript +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. + +```javascript +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: + + ```javascript + 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. |