Now that we have taken a look at the React internals (see part 1), we can start having fun with some real-life examples. In this second part of the article, we will analyze the most known mistakes that cause unnecessary re-renders or mounting/unmounting in React and eventually lead to a sluggish application.
In this article, we are going to use the React Developer Tools extension to profile the behavior of our components.
React DevTools draws a border around components that have been updated.
This gives us a hint at where the problem might be but tells us nothing about details: especially whether the update in question means “diffing” elements or mounting/unmounting them. To find out more, we need to use React’s Profiler (note it won’t work in production mode).
Add ?react_perf to any URL of your app and go to the “Profiler” tab in your Chrome DevTools. Hit the recording button and click the “Click me” button then hit “stop”.
As you can see the Profiler gives us a chart highlighting the sequences of rendering. The App component is stripped in grey meaning it did not re-render after the update, and the Counter component is colored meaning it did re-render (Note it even gives us the duration of the re-rendering).
Now we have the setup ready, let’s jump in.
The misuse of PureComponents and React.memo
As we saw in part 1, when the type of the component is a class or a function and we started the tree reconciliation process, then React will always try to look inside the component to make sure that the values returned on render did not change. In other words, it will always re-render.
Fortunately, you can tell React not to look at a certain branch, as we are confident there were no changes in it. This is done by implementing the shouldComponentUpdate (or using the React.memo if you are using functional components) which is a part of the component’s lifecycle. This method is called before each call to a component’s render and receives new values of props and state. Then we are free to compare them with our current values and decide whether we should update our component or not (return true or false). If we return false, React will not re-render the component in question and will not look at its children.
React has a built-in feature in a class called React.PureComponent (or React.memo). It is similar to React.Component, only shouldComponentUpdate is already implemented for you with a shallow props/state comparison in mind.
It sounds like a no-brainer, just swap Component for PureComponent in the extends part of your class definition and enjoy efficiency. Not so fast, though! Consider this example:
If you profile the application after clicking on the “Click me” button, you might be thinking the Welcome component shouldn't re-render as its props are constant and did not change. But if you analyze the Profiling result you get something like this:
As you can see the Welcome component is colored (light green), so it did re-render although it is a pure component and the props did not change.
The gotcha is that for every render React is creating a new object and as shouldComponentUpdate does a shallow comparison (by reference in this case) the data prop is always different.
The problem can also appear if you pass an arrow function as a prop:
So how do we solve this? Well, there are a lot of ways to do it, and it depends if you are using Class-based components or functional components.
If you are using Class-based components you can declare the props that you want to pass down to child components (functions and data) as class attributes.
If you are using functional components you can extract your logic outside the function like this:
But sometimes you can’t really extract your logic outside the function due to dependencies needed that are declared inside the component (like states …). This where the hook useCallback shines, it prevents a function from being recreated on every render:
Final important Note: Comparing two sets of props and state is not free and for most basic components is not even worth it; it will take more time to run shallowCompare than the diffing algorithm. A good rule of thumb I found: pure components are good for complicated forms and tables, but they generally slow things down for simpler elements like buttons or icons.
The children problem
Imagine you had a User component that takes UserAvatar, UserDescription, and UserContacts as children, and you want the UserAvatar component to be hideable. You might think that a code like this one is perfectly normal:
Not only this is bad code as there is a lot of code repetition, but also it’s very terrible in terms of rendering optimization. Because in terms of JSX it gets compiled to the following:
So when React runs the “diffing”, it sees that the array of children changed shape: children held a UserAvatar and now it holds UserDescription. There were no keys to compare against, so it compares types, and as they are both references to functions (and different functions), it unmounts the whole UserDescription and mounts it again, and the same applies to UserContacts. And remember the mounting/unmounting is very expensive.
Solving this mistake is very easy, we can use the short circuit boolean evaluation as follows:
And this gets compiled to the following virtualDOM:
So, UserAvatar or not, our indexes will not change, and UserDescription will, of course, still be compared to UserDescription (having references to components as type starts reconciliation anyway), but just comparing Virtual DOM is often a lot faster than removing DOM nodes and creating them from scratch again.
Don’t hurt yourself with HOCs
A higher-order component is a function that takes a component as an argument, does something, and returns a different function.
That is a very common pattern, but you need to be careful with it. Consider this :
We are creating a HOC inside of a parent’s render method. When we re-render the tree, our Virtual DOM will look like this:
As React runs the “diffing”, this time the same name references a different instance, triple equals comparison fails and, instead of a reconciliation, a full re-mount has to happen. Note it will also result in a loss of state, as described here. Luckily, it is easy to fix: you need to always create a HOC outside of render:
In this second part, we looked at the two biggest categories that cause a React application to be sluggish and slow; that is Unnecessary re-renders and even worse re-mount problems. We saw some bad code patterns that lead to these problems and how to solve them as well.
I want to finish this two-part article by saying something really important: “Don’t fret about performance optimizations until you have problems.”