How to greatly improve your React app performance

A review of common React performance pitfalls and how to avoid them

Noam Elboim
MyHeritage Engineering
6 min readNov 21, 2017

--

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:

  1. Bad shouldComponentUpdate implementations and why PureComponent won’t save you.
  2. Changing the DOM too fast.
  3. 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:

Simple shallow implementation: 'this.props.children !== nextProps.children', but it's always returning true

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:

PureComponent component is still always returning true

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:

  1. 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.
  2. 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:

shouldComponentUpdate-Children live example

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:

  1. Triggering “Layout” too much — when you can trigger Composite or Paint instead.
  2. 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:

Naive Collapse component

This component works great when it is used alone, but when you decide to use it a few more times…

Multiple Collapse starting to take a toll on the app

If you are not on mobile right now, try changing your performance to “6x slowdown” to mimic most people’s experience.

Change performance to 6x slowdown on Chrome

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:

  1. 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.
  2. 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 setting this.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:

Grouping reading & writing on Collapse opening

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:

Debounce the event

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 😄

--

--