Performance optimisations for React applications

TLDR;

The main performance hotspot in React applications is redundant processing and DOM diffing in components. In order to avoid this return false from shouldComponentUpdate as high up in your application as you can.

To facilitate this:

  1. Make shouldComponentUpdate checks fast
  2. Make shouldComponentUpdate checks easy

Video

You can watch a video recording of me talking about about this topic on YouTube

Disclaimers!

The examples in this blog will be using React + Redux. If you are using another data-flow library the principles will apply but the implementation will be different.

I have not used an immutability library in this blog but only vanilla es6 and a little bit of es7. A few things become easier when using an immutability library but I will not be discussing them here.


What is the main performance hotspot in React applications?

  1. Redundant processing in components that do not update the DOM
  2. DOM diffing leaf nodes that do not need to be updated
    - while DOM diffing is incredible and facilitates React, it is not trivial computationally

What is the default render behaviour in React?

Let’s take a look at how React renders components

Initial render

On the initial render we need the entire application to render
(green = nodes that rendered)

Every node has rendered — this is good! Our application now represents our initial state

Proposed change

We want to update a piece of data. This change is only relevant to one leaf node

Ideal update

We want to only render the nodes that are along the critical path to our leaf node

Default behaviour

This is what React does if you do not tell it otherwise
(orange = waste)

Oh no! All of our nodes have rendered.

Every component in React has a shouldComponentUpdate(nextProps, nextState) function. It is the responsibility of this function to return true if the component should update and false if the component should not update. Returning false results in the components render function not being called at all. The default behaviour in React is that shouldComponentUpdate always returns true, even if you do not define a shouldComponentUpdate function explicitly.

This means that by default every time you update your top level props every component in the whole application will render. This is a major performance problem.


How do we get the ideal update?

Return false from shouldComponentUpdate as high up in your application as you can.

To facilitate this:

  1. Make shouldComponentUpdate checks fast
  2. Make shouldComponentUpdate checks easy

Make shouldComponentUpdate checks fast

Ideally we do not want to be doing deep equality checks in our shouldComponentUpdate functions as they are expensive, especially at scale and with large data structures.

An alternative approach is to change an objects reference whenever it’s value changes.

Using this technique in a Redux reducer:

If you adopt this approach then all you need to do in your shouldComponentUpdate function is do reference checks

Example implementation of isObjectEqual


Make shouldComponentUpdate checks easy

Example of a hard shouldComponentUpdate

Structuring your data like this makes doing checks in your shouldComponentUpdate hard

Problem 1: huge shouldComponentUpdate functions

You can see how big and complex the shouldComponentUpdate is with a very simple data. This is because the function needs to know about the data structures and how they related to one another. The complexity and size of shouldComponentUpdate functions only grows as your data structure does. This can easily lead to two errors:

  1. returning false when you should not (state is not correctly represented in the app)
  2. returning true when you should not (performance problem)

Why make things hard for yourself? You want these checks to be so easy that you do not need to really think about them.

Problem 2: tight coupling of parents to children

Generally applications want to promote loose coupling (components know as little about other components as possible). Parent components should have as little understanding as possible about how their children work. This allows you to change the children’s behaviour without the parent needing to know about the change (assuming the PropTypes remain the same). It also allows children to operate in isolation without needing a parent to tightly control it’s behaviour.

Fix: Denormalize your data

By denormalizing (merging) your data structure you can go back to just using really simple reference checks to see if anything has changed.

Structuring your data like this makes doing checks in your shouldComponentUpdate easy

If you want to update the interaction you change the reference to the whole object


Gotcha: reference checking and dynamic props

An example of creating dynamic props

Usually we do not create new props inside a component to just pass it down. However, it is more prevalent to do this inside of loops

This is commonly used when creating functions

Strategies to overcome the issue

1. Avoid creating dynamic props inside of components

improve your data model so that you can just pass props straight through

2. Pass dynamic props as types that satisfy === equality

eg:
- boolean
- number
- string

If you really do need to pass in a dynamic object you could pass a string representation of the object that could be deconstructed in the child

Special case: functions

  1. Do not pass functions if you can avoid it. Rather, let the child dispatch actions when it wants to. This has the added advantage of moving business logic out of components.
  2. Ignore functions in your shouldComponentUpdate check. This is not ideal as it won’t be able to know if the value of the function changes.
  3. Create a map of data -> function that does not change. You could put these in state in your componentWillReceiveProps function. That way each render would not get a new reference. This method is extremely heavy handed as you need to maintain and update a list of functions.
  4. Create a middle component with the correct this binding. This is also not ideal as you introduce a redundant layer into your hierarchy
  5. Anything else you can think of that avoids creating a new function every time render is called!

Example of strategy #4


Tooling

All of the rules and techniques listed above were found by using performance measuring tools. Using tools will help you find performance hotspots specific to your application.

console.time

This one is fairly simple:

  1. start a timer
  2. do stuff
  3. stop the timer

A great way to do this is with a Redux middleware:

Using this method you can record the time taken for every action and its resulting render in your application. You can quickly see what actions take the longest amount of time to render which gives you a good place to start when addressing performance concerns. Having time values also helps you to see the what difference your changes are making to your application.

React.perf

This one uses the same idea as console.time but uses the React performance utility tools:

  1. Perf.start()
  2. do stuff
  3. Perf.stop

Example Redux middleware:

Similar to the console.time method this will let you see performance metrics for each of your actions. For more information on the React performance addon see here

Browser tools

CPU profiler flame charts can also be helpful in finding performance problems in your applications.

The Flame Chart shows you the state of the JavaScript stack for your code at every millisecond during the performance profile. This gives you a way to know exactly which function was executing at any point during the recording, how long it ran for, and where it was called from — Mozilla

Firefox: see here

Chrome: see here


Thanks for reading and all the best in making highly performant React apps!

Update

I have created a follow up post: Performance optimisiations for React: Round 2. It contains some evolutions on the ideas presented in this blog and which can result in significant performance improvements in your React applications. Enjoy!