How to greatly improve your React app performance
A review of common React performance pitfalls and how to avoid them
Performance problems in web apps are not new.
Everyone knows that moment when you take a new component, add it to your app — and suddenly every single user interaction you attempt has a noticeable performance lag! Sometimes, you can even use the same component multiple times and get an embarrassing animation.
You might think up a few choice nicknames for whoever wrote that component, but here’s a better idea: Do something about it — really, you can!
We will tackle the following common React pitfalls:
- Bad shouldComponentUpdate implementations and why PureComponent won’t save you.
- Changing the DOM too fast.
- Using events and callbacks without limitations.
In each, we first explain the root of the problem, and then we present some simple ways to avoid it.
Own your shouldComponentUpdate
The component lifecycle hook shouldComponentUpdate is meant to prevent unnecessary renders. shouldComponentUpdate gets the next props and state as arguments, and if it returns true, the render function will be executed. Otherwise, it won’t.
The default implementation for React.Component is return true
.
More renders means updates take more time, so we prevent unneeded updates to reduce that extra time. To do so, you’d think we’d want to implement strict shouldComponentUpdate functions to the extent we can, right?
The Problem
Let’s try using a very simple shouldComponentUpdate implementation:
Wait, why it doesn’t work?
It doesn’t work because React is creating a new instance of ReactElement on each render!
This means shallow comparison like return this.props.children !== nextProps.children;
in a shouldComponentUpdate function, is almost as good as writing return true;
In my experience, most components usually support ReactElement props (PropTypes.node or PropTypes.element) in some way or another, like children
so this is a common case.
And what about PureComponent?
React.PureComponent is an alternative to React.Component. Instead of always returning true in its shouldComponentUpdate implementation, it returns the outcome of shallow props and state comparison.
Using PureComponent will result in the same outcome:
Is this is a bug or a feature of PureComponent? I can’t be sure. What we do know is that PureComponent is not useful in most real cases, and will not prevent updates.
Possible Solutions
The first thing that may come to mind is — let’s make a deep comparison! This actually works, but it has 2 major cons:
- Running a deep comparison can be a long, heavy, slow process by itself, and the render function will not execute until the shouldComponentUpdate function finishes running.
Performance may therefore deteriorate even further. - It depends on the current implementation of React Elements, and may break in future versions.
Therefore, in my opinion, using a deep comparison is not a very good solution.
In searching for a better solution, I looked at how other libraries with Virtual DOM have already been handling this problem.
I found a very interesting comment by Evan You (Vue.js creator) in a feature request to add a React-like shouldComponentUpdate to Vue.js; He explained that this can’t be solved by “diffing” Virtual Elements, as it is likely to have many edge cases. Relying on React Elements to detect state change in your component is therefore not a viable solution.
Taking that into a practical use — React Elements should be skipped in shouldComponentUpdate implementations. Instead, use some sort of a state change to indicate that the component should be updated.
Instead of checking the comparison this.props.children !== nextProps.children
, we will depend on a different prop to indicate a state change, preferably a string/numeric one, in order to make the comparison a very fast one.
We may even use a new prop, specially designated to indicate that the component should be updated.
Taking this even further, my colleague (Tzook Shaked) and I created a HOC that uses Inheritance Inversion to extend components with a generic shouldComponentUpdate implementation, a PureComponent alternative that actually works.
You can check the code here: https://github.com/NoamELB/shouldComponentUpdate-Children
I should note that it’s a generic implementation, and therefore it may not fit for all situations. Read more about it in the repository’s readme.
You can also see in the live example below that the only one using a custom shouldComponentUpdate implementation, as proposed here, doesn’t render unnecessarily:
Allow your components to scale up
Are you using the same component multiple times in your app and it is that making the app very laggy? Do animations look crappy? Sometimes, it only takes one use to result in a performance toll on the whole app?
The Problem
When creating complex components, you may need to do some custom DOM manipulations. In creating those, you may encounter the following two problems:
- Triggering “Layout” too much — when you can trigger Composite or Paint instead.
- Layout Thrashing — where you trigger unnecessary DOM recalculations by reading from the DOM right after you have written to it multiple times.
Let’s look at a naive Collapse component, changing the height between 0 and the content height:
This component works great when it is used alone, but when you decide to use it a few more times…
If you are not on mobile right now, try changing your performance to “6x slowdown” to mimic most people’s experience.
Possible Solutions
Let’s analyze what is happening in the Collapse — this is the part where we change the height:
There are 2 things we should notice:
- We are changing height, which according to csstriggers.com is triggering a Layout recalculation. If we managed to change something like transform instead, that would only trigger Composite and should be much smoother, right?
Indeed, that would perform better, but it would also leave a blank space under the Collapse, since we would then never be changing its height. - Line 3 is a classic example of Layout Thrashing: We do a read from the DOM by
this.contentEl.scrollHeight
, and then a write to the DOM by settingthis.containerEl.style.height
. Multiply it by the number of Collapse components.
Wouldn’t it be nice if we could group all the reads, perform them together, and after that do all the writes?
Batching together DOM readings and writings is a good trick to deal with Layout Thrashing, and we can use requestAnimationFrame to do the batching in the following manner:
This can be very cumbersome. Instead, use inside components, or check Fastdom library and use it instead.
It is worth mentioning that you may not always get a good enough performance, as you are limited by the browser and device capabilities. In those cases, your solution may be a product change.
For example: Yes, browsers find it hard to open a thousand Collapse components at once, but do you really need to open all 1,000 on the screen at once?
One last thing: You may hear about something called will-change. It may help you in specific cases, but you also risk lesser performance. Take care not to overuse it.
Put your callbacks on a leash
Having a debounced or throttled version of our functions is useful when we attach any DOM event. It lets us reduce the number of calls to this function to the bare minimum we wanted and thereby improve the performance.
Write something like this:window.addEventListener(‘resize’, _.throttle(callback))
is very common. But why don’t we use it in components callbacks as well?
The Problem
Let’s look at the following component:
Have you noticed that we call this.props.onChange
on every change? This is called a lot of times, where most of the calls are unnecessary. If the parent is making DOM changes, or any other heavy operations, according to the onChange
callback, we may start seeing a lag in user interactions on the app.
Possible Solution
Instead, we can implement something like this:
Now it calls the props.onChange
callback only after the user has finished typing, and prevents a lot of unnecessary events along the way.
(You can read about the differences between _.throttle
and _.debounce
here)
In Conclusion
These tools should help you handle the performance pitfalls we can encounter in a React app. By using shouldComponentUpdate wisely, controlling the changes you do to the DOM, and putting your callbacks on delay with debounce/throttle, you can improve your app’s performance greatly.
If you want to test all of those in real life situations, check out UiZoo. It’s a dynamic component library for React components, and it parses your components and showcases them for you to either develop, test, or share with others.
Thank you for reading. Drop me a line and let me know if this helped you 😄