We migrated our web app to React almost 3 years ago, and since React performs extremely well out of the box, optimizing performance was not something we had to worry about. However, as our application grew in features and complexity over the years, the app’s performance wasn’t keeping up as well as we wanted it to.

Identifying the problem

We profiled our app with various tools, but the one that was the most useful was the React Perf addon. There are many great resources in that link on how to use the tool, so we won’t go over the details here. We measured the wasted rendering performance of navigating to our most used and most complex page. Here is the output of Perf.printWasted() :

The list goes on for over 100 rows, with a total of 6.5 seconds of wasted rendering time. I’ll let that number sink in for a second…

Fixing the problem

After the initial shock wore off, we became excited at how much potential there was to improve our application. Fixing the issue is pretty simple*. We simply need to short circuit the re-rendering for a subtree if we know that the subtree hasn’t changed.

This can be done by returning false in the shouldComponentUpdate(nextState, nextProps) lifecycle hook. The typical implementation is to do a shallow equality comparison of state and props to the nextState and nextProps, and return false if all values are equal. This functionality is provided to you out of the box with PureComponent for ES6 classes and PureRenderMixin for legacy components.

So all you have to do is use PureComponent in the right places and you’re good to go. There’s nothing more to it. Enjoy your new blazing fast React app!

*Except…

Remember when I said that fixing the issue is pretty simple? I lied. It could be simple if your codebase followed all of the modern React best practices. However, with 3 years of code written without these best practices in mind, our codebase was far from being ready for PureComponent .

So what does it mean to “be ready” for PureComponent ? Below is a list of gotchas that you need to watch out for.

Mutations

This is fake code to illustrate the example. Assume the component instance is the same in both renders.

Q: When the pure component is asked to re-render the second time, will it re-render the new data?

A: No. PureComponent does a shallow object comparison on the props, and since you are passing in the same object reference, shouldComponentUpdate will return false and the updated data will not be rendered.

This means that if any part of your app mutates data that needs to be rendered, PureComponent puts you at risk of displaying stale data. This is by far the biggest blocker because it results in incorrect UI behavior, and it is outside the control of your component.

Fix: Never mutate values used as React props or state. You can do this with the help of array spreading, object spreading, immutability-helper, immutablejs, etc. Good luck…

External data dependencies in render

Q: What happens when the data in Store changes?

A: Nothing. Even if some ancestor component triggers a re-render, PureParent will not re-render because neither its state or props has changed.

This gotcha also leads to incorrect UI behavior, but the issue is scoped to the component itself, making it easy to fix.

Fix: Extract data dependency into state or props, and update state or props when the data changes.

Object copying

Q: What happens when Parent is re-rendered with an unchanged props.dataList ?

A: PureChild is re-rendered wastefully because filter returns a new object, which tricks PureChild into thinking that the prop has changed.

This problem is triggered by calling any function that creates a new object or using an inline object literal in render .

Fix: Cache your derived data calculations. This can be done manually in componentWillUpdate or using the awesome reselect library.

ES6 arrow function / bind

Or onClick={doSomething.bind(this)}

Q: What happens when Parent is re-rendered?

A: PureChild is re-rendered wastefully because arrow functions and Function.prototype.bind return new function instances, which tricks PureChild into thinking that the onClick handler has changed.

Fix: Don’t use arrow functions and bind in render. Extract them out to instance methods.

Update (11/1/2017): Freely use arrow functions / bind without worrying about wasteful re-renders with reflective-bind . Read more about it here!

For more complicated cases where you need to use variables within the render context, you may need to introduce another component layer that takes in the relevant context variables as props, and calls your callback with the variables.

Data takes in the index as a prop so we don’t have to do this.handleDelete.bind(this, i)

forceUpdate()

This bypasses shouldComponentUpdate . Just don’t do this…

Spreading props

Q: You know the drill. Why is this bad?

A: You may be accidentally passing props that PureChild doesn’t care about. When those props change, PureChild will re-render wastefully.

Fix: Unless you’re writing some sort of higher-order-component, don’t be lazy and be explicit about the props that you are passing.

Conclusion

Turns out our app is littered with every single one of these gotchas, and is especially dependent on mutation side-effects. We are now in the process of removing some deeply rooted dependencies on mutations and getting the codebase to a state where we can incrementally fix and migrate individual components to pure components. We will post more about our progress and learnings in Part 2. Stay tuned!

Follow the discussion or upvote on Hacker News!