React re-rendering, PureComponents and memoization
In React, re-rendering Components can cause performance issues, especially when dealing with deep Component trees. We will see what can cause wasteful renders and how to avoid them.
The rendering cycle
Here are the main events that leads to a Component update :
- A change of the Component props or state
- Another Component updating higher in the tree
Both of those things will trigger the shouldComponentUpdate()
method, which decides if the rendering lifecycle should proceed.
By default shouldComponentUpdate
returns true, causing React to update its virtual DOM. We can do a first optimization here by using PureComponent
in children directly under components subject to a lot of renders, typically containers.
Optimizations
PureComponents
PureComponent
includes by default a shallow comparison of props and state in shouldComponentUpdate
, meaning it won’t re-render with the same props and state.
However, the shallow compare will fail and still trigger an update if a prop is dynamically generated:
In the example above, even if the 3 props passed are not used and hold the same values, each will fail the shallow compare as they will be generated every time Container re-renders: list.filter returns a new Array, and the unnamed inline function and object will have new references.
Extracting elements
The last two can be optimized by naming and extracting them:
We are left here with only one unnecessary render caused by filteredList.
We could revert the Child to a simple Component and do a deep equality check in shouldComponentUpdate
, but it is a bad idea overall as comparing nested structures can be very costly.
Luckily there’s an awesome tool for that: data memoization !
Memoizing your data
Memoization is a powerful tool that acts as a cache: it takes a pure function as a parameter and is guaranteed to return the result of the last invocation for the same input. In other words, with the same input it will return the memorized result of the last time you called it.
The performance benefits can be huge: it avoids computing costly operations over and over, and in our use case it passes the shallow equality prop comparison as our list Array will have the same reference!
There is quite a few modules that helps you memoize your data, and we chose memoize-one as it is lightweight, fast and simple to use.
Here is the same Container with memoization:
Voila ! Now the Child Component re-renders only when the Container list prop changes, as expected.
Disclaimer: your function has to be pure! The example below will not work:
this.filterList()
will always return the same memoized result here even if this.props.list
changes because it will always have the same (empty) parameter.
Summing it up
You can avoid a lot of wasteful renders by combining simple optimizations :
- using
PureComponent
in direct children of your container - extracting the inline function props as methods
- extracting your objects
- memoizing nested data in your container
Our use case at OVRSEA was in the reporting tab of our app, where one of our container Component could re-render between 5 and 10 times on the first mount and 2 times when updated, with every child below it.
It was not only slow but also made our charts buggy: triggering a re-render in the middle of an animation led the graph to flash and not having any label.
In action, this graph before and after optimizations with Update Highlights turned on in React DevTools :
Hope this helps ! Further reading :
https://reactjs.org/docs/reconciliation.html
https://marmelab.com/blog/2017/02/06/react-is-slow-react-is-fast.html