In this article, we look into how React checks if the view should be updated. We also use the tools like the shouldComponentUpdate lifecycle function, the PureComponent, and the memoization. We start by acquiring a basic understanding of how React works under the hood. It is vital in attempting to improve the React performance.
What might cause the component to update?
The most basic rule is that the component updates when either the props or the state changes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class Keyboard extends Component { state = { keyPressed: null } componentDidMount() { document.addEventListener('keyup', this.keyPressed); } componentWillUnmount() { document.removeEventListener('keyup', this.keyPressed); } keyPressed = (event) => { this.setState({ keyPressed: event.code }) } render() { console.log('render called'); return ( <div> <p>{this.state.keyPressed}</p> </div> ) } } |
See the Pen
Basic react App by Marcin Wanago (@mwanago)
on CodePen.
When you take a look in the console in Developer Tools, you can see that the render function runs every time, even if you click on the same button twice and the message displaying on the screen does not need to change. The above is caused by the fact that React can’t be sure if the render function output is different just yet.
Reconciliation
When we change the state or the props, React reruns the render method and compares it to the older output. To do that, it uses the reconciliation algorithm.
- If the elements are of a different type, Reacts completely removes the old tree and builds one from scratch. Changing <div> to a <span> causes the React to rebuild the element completely with all its nodes
123 <div><Keyboard /></div>
123 <span><Keyboard /></span>In this example, the <Keyboard /> is destroyed and remounted.
- If the elements are of the same type, React keeps the old DOM node and only updates the attributes that change.
1 <div className="element" />
1 <div className="element active" />In the above example, the DOM element isn’t removed and React only changes the class
If React doesn’t scrape the old tree, it repeats the above algorithm for all the children.
If you want more information on the topic, check out the official docs on the reconciliation
Increasing the React performance
As you might have guessed, the algorithm described above can be pretty costly when it comes to computing power. In our example with the Keyboard component, every time we change the state by clicking a keyboard button, the render method runs. Now you know that this causes the reconciliation algorithm to work. React compares the old output of the render function to the new one, and this process takes places even if the render function returns the same output. We have some tools that we can use to change the above behavior.
shouldComponentUpdate
The shouldComponentUpdate function is a part of the React component lifecycle. It runs before rendering and it returns true by default. If it returns false, it prevents the rerendering and the reconciliation algorithm doesn’t run at all. Its arguments are the upcoming props and the new state.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import React, { Component } from 'react'; class Keyboard extends Component { state = { keyPressed: null } componentDidMount() { document.addEventListener('keyup', this.keyPressed); } componentWillUnmount() { document.removeEventListener('keyup', this.keyPressed); } keyPressed = (event) => { this.setState({ keyPressed: event.code }) } shouldComponentUpdate(nextProps, nextState) { return this.state.keyPressed !== nextState.keyPressed; } render() { console.log('render called'); return ( <div> <p>{this.state.keyPressed}</p> </div> ) } } |
See the Pen
Keyboard by Marcin Wanago (@mwanago)
on CodePen.
In the example above, when we press the same button twice in a row, the shouldComponentUpdate function returns false the second time. Thanks to that, the render function does not run and neither does the reconciliation algorithm. If our component would have any children, React wouldn’t attempt to rerender any of them either. It can be a major boost to the performance of your application.
The shouldComponentUpdate function does not run for the initial render or when you use the forceUpdate function.
PureComponent
The PureComponent is similar to the regular components. It shouldn’t be used with shouldComponentUpdate because it has its own approach to it.
PureComponent shallowly compares upcoming props and the new state. If the new props and the new state is the same according to this, the component doesn’t rerender.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import React, { PureComponent } from 'react'; class Keyboard extends PureComponent { state = { keyPressed: null } componentDidMount() { document.addEventListener('keyup', this.keyPressed); } componentWillUnmount() { document.removeEventListener('keyup', this.keyPressed); } keyPressed = (event) => { this.setState({ keyPressed: event.code }) } render() { console.log('render called'); return ( <div> <p>{this.state.keyPressed}</p> </div> ) } } |
See the Pen
Keyboard by Marcin Wanago (@mwanago)
on CodePen.
The crucial thing about the statements above is the fact that it is a shallow comparison. That means that if you have some deeply nested structures, it might behave in unexpected ways.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
keyPressed = (event) => { this.setState({ key: { code: event.code } }) } render() { console.log('render called'); return ( <div> <p>{this.state.key.code}</p> </div> ) } |
An example of such a situation is the code above. In the keyPressed function, we create a brand new key object every time.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const state = { key: { code: 'KeyA' } } const newState = { key: { code: 'KeyA' } } console.log(state.key === newState.key); // false |
Memoization of the function components
All the React performance improvements that we’ve covered so far work with class components. Functional components have a performance optimization similar to the PureComponent called memoization. It is not a new concept in programming. It involves caching the result of a function that might be costly in terms of computing power.
Let’s inspect it on an example of a factorial function.
1 2 3 4 5 6 |
function factorial(n){ if(n === 1 || n === 0){ return 1; } return factorial(n - 1) * n; } |
If we try to calculate if using some big numbers, it might take some time. Why repeat that effort if we ever need the same factorial again?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const cache = {}; function factorial(n){ if(cache[n]) return cache[n]; if(n === 1 || n === 0){ cache[n] = 1; return cache[n]; } else { cache[n] = factorial(n - 1) * n; } return cache[n]; } |
In the simple example above, we check if that particular factorial is already calculated. Thanks to that, we can pull it from the cache.
Using React.memo
We can apply a similar concept to the React components. Why should we render the component with the same props again, if we already did that?
When we use the memo higher-order-component, React keeps track of the renders of our component. It won’t render the component from scratch if it is currently rendered for the same props.
Keyboard.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import React, { Component } from 'react'; import Key from './Key'; class Keyboard extends Component { state = { pressedKey: null } componentDidMount() { document.addEventListener('keyup', this.keyPressed); } componentWillUnmount() { document.removeEventListener('keyup', this.keyPressed); } keyPressed = (event) => { this.setState({ pressedKey: event.code }) } render() { return ( <div> <Key code={this.state.pressedKey} /> </div> ) } } export default Keyboard; |
Key.js
1 2 3 4 5 6 7 8 9 10 |
import React, { memo } from 'react'; function Key ({code}) { console.log('Key component rendered'); return ( <p>Pressed key: {code}</p> ) } export default memo(Key); |
When we fiddle with the code above, we notice that it stores the previous render of the Key component and doesn’t render it again if the new props are the same. By default, it performs a shallow comparison just like the PureComponent. We can provide an additional custom comparison function, and if it returns true, the component does not render again. It is opposite to the shouldComponentUpdate function.
1 2 3 4 5 6 7 8 9 10 |
function Key ({code}) { console.log('Key component rendered'); return ( <p>Pressed key: {code}</p> ) } function areEqual(props, nextProps) { return props.code === nextProps.code; } |
Even though the memo function is intended to use with functional components, it works also with class components
Summary
Even though our intention might be to improve the React performance, we need to watch out not to introduce bugs to our code. To prevent that, we need to be aware of how the React library works and how all the parts of our code fit together with it. By using the knowledge from this article, the performance of our application can increase a lot.