Optimizing React Performance

How We Eliminated 13,000+ Wasted Renders

Michael Haglund
Open Digerati

--

Photo by Artem Sapegin on Unsplash

You’ve spent months, maybe even years, building an application for your organization. It looks great, has gone through a rigorous QA process, and has gotten good reactions from your beta testers.

Launch day comes; are you certain your application can stand up to unexpected demands, high messaging rates, or slow data connections?

In software development it’s easy to push addressing performance concerns towards the end of the process (maybe even when it’s too late). However, there are tools readily available to help you identify, and mitigate performance issues early on.

As our application began to grow we started noticing our animations drop lower than 60fps and visually poor mounting/rendering performance. Our application also contains a large chat feed with multiple channels to chat in. As the messages in each channel began to grow (hundreds, or even thousands) we would notice poor scrolling performance and poor rendering performance as the user switched between channels.

By using the tools and methods below, our team was able to identify and eliminate 13,000+ wasted renders in initial application load, and thousands of additional wasted renders post-load in the Church Online Platform.

React uses many techniques to minimize the number of DOM operations for us already. For many applications, if you are using the production build, you may already meet or surpass your performance expectations. Nevertheless, there are several ways you can speed up your application.

Identify Performance Issues

In react-dom 16.5+ the React team has provided enhanced profiling capabilities in DEV mode through the React DevTools. This is always the first tools that I grab when investigating potential performance optimizations. You can find them here:

1. React DevTools Profiler

If you’re benchmarking or experience performance problems with a specific component, the React DevTools Profiler is usually the first place I will look. There is an excellent blog post and video walkthrough that goes into great detail on using the profiler to gather performance data.

2. React DevTools Update Highlighting

React maintains a virtual DOM that it reconciles against to determine which parts of the UI need to re-render based on props or state changing. This is great, but it also means we don’t really know which parts of our application are updating at any given time. In the React DevTools there is a setting you can turn on that will visually highlight elements on the screen as they render (or re-render).

As you can see in the form below, when typing in an input field the entire form is full of wasted renders. Later on, we’ll learn some methods and tools that can help prevent this from happening.

3. Why Did You Render

Great, we know that our application is re-rendering more times than necessary, how do we know the root cause of the re-render? Luckily, there’s an incredible utility called @welldone-software/why-did-you-render that “monkey patches React to notify you about avoidable re-renders”. After configuring why-did-you-render your console will fill with information to help you track when and why certain components re-render (and common fixing scenarios). You can test out the library in their official sandbox.

Mitigate Common Performance Issues

After we identified potential performance issues in our application, we found that there were several common scenarios that could be addressed.

Avoiding Reconciliation

As mentioned earlier, React maintains an internal virtual DOM representation of your application that it reconciles against. Any time state, props, or a parent are updated, it tells impacted components to re-render. If you configured why-did-you-render you likely saw several messages relating to components updating when their props/state object references changes, but the values did not. This is a good example of where we can tell React that in this case it does not actually need to re-render the component, because the result will be the same. React provides three methods to do this: shouldComponentUpdate , React.PureComponent , and React.memo .

Evaluate Prop Passing
Looking deeper into the immutability and passing of our props had the most noticeable impact on our application performance. You might be surprised to know that creating a component like below will cause it to always re-render, which is why it’s always import to do costly calculations or define constants outside of the render method:

function App (items) {
return (
<BigListComponent
style={{width: '100%'}}
items={items}
/>
);
}

In order to prevent this component from always re-rendering, we should declare the style variable outside of the render method:

const bigListStyle = { width: '100%' };function App (items) {
return (
<BigListComponent
style={bigListStyle}
items={items}
/>
);
}

The same can be applied to passing an inline function as a prop:

// BAD: Inline function
function App (items) {
return (
<BigListComponent
onClick={() => dispatchEvent()}
/>
);
}
// GOOD: Reference to a function
const clickHandler = () => dispatchEvent();
function App (items) {
return (
<BigListComponent
onClick={clickHandler}
/>
);
}

shouldComponentUpdate
The shouldComponentUpdate lifecycle method is one you may have just glanced over when initially learning React. It is triggered before before the re-rendering process starts. If the function returns true, React continues with the rendering process. If the function returns false, the rendering process is cancelled.

shouldComponentUpdate(nextProps, nextState) {
if (this.state.count !== nextState.count) {
return true;
}
return false;
}

React.PureComponent
In most cases, instead of writing the shouldComponentUpdate, you can change your component to inherit from React.PureComponent instead. This does a shallow comparison of the current/previous props and state.

class Button extends React.PureComponent {
...
}

React.memo
React.memo provides similar functionality if you use function components instead of class based components.

function Button (props) {
/* ... */
}
function areEqual (prevProps, nextProps) {
return prevProps.count === nextProps.count;
}
export default React.memo(Button, areEqual)

Virtualizing Long Lists

In order to address the issue with our long chat feed, the React team recommends a technique called windowing. This technique only renders the portion of the list that is visible to the user (+/- a given offset) in order to reduce the time to render. As the user scrolls, new list items are retrieved and rendered. react-window and react-virtualized are two libraries that provide components to help with list virtualization.

Conclusion

Hopefully after reading this article you are better equipped to identify and mitigate potential performance issues in your applications. Our team is better equipped when creating components moving forward, and I hope yours is too.

Additional Resources
https://reactjs.org/docs/optimizing-performance.html
https://github.com/welldone-software/why-did-you-render

--

--