Easily optimise your React (Native) Components rendering
Goal: this article aims to make it clear how React rendering works, and how you can easily reduce the number of renders your components do, and automatically improve your application / UI performance.
Note: you don’t need to go into optimisations too early, or if you think you don’t need it (remember, “early optimisation is the root of all evil”) but it doesn’t hurt to fully understand from the beginning how React render() works, especially since some of those optimisations are fairly easy to achieve, and they can be implemented as coding guidelines in your projects.
Note2: I’m using in my example ReactNative, because I do most of my work in ReactNative 🙈, but the same applies for React on the web. (right? 😂)
So, how does React render() work ?
The render()
method gets called as we know each time props or state change on our component. This will then trigger a render()
on all the children components (if they are not optimised). React will apply the reconciliation algorithm, and determine if some new components have to be rendered, unmount components that are not needed anymore, or update some of the existing components. BUT, a render() will be triggered in all the child-components, React will still perform the diffing, even if in the end it will decide that this component, and all it’s subcomponents don’t need to be updated at all.
So as you can imagine, if you have a complex view hierarchy, and also a lot of state or props updates, triggered by various events, or redux store updates, this can result in a lot of unnecessary renders and computations.
To make this clearer, let’s take a simple example. Let’s create the classic ‘React counter’ example, but let’s adjust it for our purposes, and let’s have 2 counters instead of 1. We will create a Counters
component to hold the state of our 2 counters and 2 buttons to increment the state, and let’s make a CounterDisplay
component that just displays the each counter.
(I will be using class components and pure JS instead of Typescript just to keep the examples clear and easy to understand. I will at the end of the article provide also the code that uses functional components using hooks)
So a lot of people would expect here, if we press the first button, to increase just counter1 that just our first CounterDisplay
gets re-rendered, but in fact both our CounterDisplay
will get re-rendered (and all other child components of our Counters
component)
As said before, this shouldn’t be a problem for such a small component tree, but if our CounterDisplay
components would be more complex and instead of 2 we have 20–30 , this can easily affect performance. So let’s see how we could optimise in our case our CounterDisplay
to avoid un-necessary renders.
Optimisation 1: extend PureComponent
We should make our CounterDisplay
extend from PureComponent .
PureComponents
automatically implement shouldComponentUpdate
() and will not re-render itself and subsequently all it’s children if it does not detect a change in state
or props
.
Warning: PureComponent
will do a shallow check on your props
and state
, so make sure you do not mutate those, because your components will not re-render.
If after we extend from PureComponent
and we now increment counter 1, you will see that just the first CounterDisplay
will get re-rendered.
Ok, we have now our brand new optimised CounterDisplay
but we want to make it prettier. Let’s add some styles to it :
Ooops! We broke our component again. Every render()
in the parent component will trigger re-renders on both our CounterDisplay
components. Can you figure out why ?
Optimisation 2: use StyleSheet for styles
On every render()
we are creating a new style that we pass to our CounterDisplay
component. Even though the style is exactly the same, it will be a new object, and our component will be re-rendered. So instead we should create a StyleSheet
which will preserve our styles.
Great, now our component it’s fixed. Let’s go ahead and for the sake of this article make our CounterDisplay
component interactable. Let’s adjust it so that it will take an onPress
function, which will be invoked with the counter value.
Well what do you know ? We ruined our optimisations again.
Optimisation 3: avoid passing arrow functions to components
With our new callbacks in place, during every render() inside the parent component Counters
we are creating a new arrow function that we pass as a property down to our CounterDisplay
components which will cause them to re-render because React detects that it received a ‘new’ property, even though the new function is exactly the same as the previous one.
So, we should instead create our callback as a constant, or a function reference in our parent Counters
component.
Now, each time we render() the Counters
component, we will pass down as an onPress
callback the same function, so we won’t trigger un-necessary renders in our CounterDisplay
components.
Optimisations when using Hooks
Now let’s take the same example but re-write it using hooks. Our code should look something like this.
(I already created the StyleSheet as that is straightforward, and the same when using hooks as well)
With our new functional components that use hooks, now we have the same issues. Too many un-necessary renders are performed. Let’s see how we can fix them.
Hooks optimisation 1: React.memo
As seen in the docs , React.memo
is the same as using PureComponent
, but for function components. Just what we needed ! Who would have thought !? 😆
The update is simple, just wrap our CounterDisplay
function with React.memo
const CounterDisplay = React.memo(props => {
...});
Now our component CounterDisplay
shouldn’t re-render if props don’t change, but the problem is that we still pass a new onPress
function on each render()
from our parent Counters
component. Let’s also fix that.
Hooks optimisation 2: React.useCallback
To ‘memoize’ our callbacks, we can use the useCallback
function which will return a ‘memoized’ callback as described in the docs.
The code to do this for our component will look something like this:
const onCounterPress = useCallback(value => alert(value), []);
That’s it 👏 ! Now our functional components are also performant as their class components counter-parts.
If you want to play around with the code, you can find it in this Expo snack. 💻
Conclusions
First and foremost, remember : “Early optimisation is the root of all evil” .
But, if you feel your app performance can be improved, you should start profiling it and if you feel like components re-rendering affects your app performance you can start looking into optimising some of them. You might also find useful this npm package that can notify you about un-necessary, avoidable re-renders.
Also, if you are in the beginning of your project, some of those things are not that hard to do, and don’t add too much complexity, so you could probably include them in your project coding guidelines.
Let me know if this clarified some of the miss-conceptions about React rendering, and if all the suggestions are clear and make sense to you. 🙏