My React App is Slow. What Should I do?

David Han
In the weeds
Published in
7 min readNov 8, 2019

--

Image Credit: Carolyn Jao

I recently attended another great workshop by Kent C. Dodds, this time on React performance (I wrote about his workshop on React hooks here). It seems that Kent’s workshops have been my blog post fodder recently 😄, but I do highly recommend them to anyone who is interested in any of his workshop topics.

During the Q&A portion of the workshop, I asked Kent, “If you were given a slow React app with many different kinds of performance issues, how would you approach fixing them?” In other words, how would he prioritize the different approaches for fixing React performance issues? His answer to my question is how I’ve organized the rest of this blog post. There are many ways you can fix performance problems, but I’ll share the three main ways to fix the majority of performance problems you’ll encounter. In order of importance, these are: state colocation, fixing slow renders, and fixing unnecessary renders.

State Colocation

You can get the biggest bang for your buck when it comes to React performance by colocating your state. This means moving your state as close to where you need it as possible, especially if you have a lot of unnecessary state in parent components. By writing idiomatic React using state colocation, you get both performance and maintainability benefits. When most of your state is in a global context, or your parent components have more state than they need to know about, then your components are more complex than they need to be and it makes the overall code harder to reason about.

Let’s look at an example of colocating state. When you type into the search input below, you’ll notice that it’s very slow and you can’t type without a noticeable delay.

This delay is caused by having the inputValue state at the same level as the Menu component. The Menucomponent contains a list of 807 pokemon, all of which are re-rendering when inputValue changes. This is the nature of React, where all children components are re-rendered based on state or prop changes. Since our search input is not related to the pokemon Menu component, we can extract a SearchInput component and move the inputValue state to the SearchInput component.

After extracting the SearchInput component, you’ll see that we can now type in the search input more quickly. By moving the search input related state as close to where you need it as possible, we are no longer causing the extra re-renders within the Menu component since the state is no longer on the same level.

By extracting the SearchInput component, we have improved the performance of our components and, by lowering the overall complexity of the code, we’ve also improved the maintainability as well.

Fixing Slow Renders

We can try to fix as many “unnecessary renders” as we can, but it’s hard to avoid the fact that we’ll likely be re-rendering components quite often due to the business logic that requires state changes in our applications. Since we can’t avoid re-renders in many cases because of the nature of how React works (components re-rendering based on prop or state changes), we should make our render functions fast.

Before moving forward, I should note that when using React hooks, everything within the functional component is considered a part of the render function.

function SomeComponent() {
// Everything in here including the jsx being returned is considered a part of the render function.
const [someState, setSomeState] = React.useState(''); return <div>{someState}</div>;
}

With that in mind, let’s look at an example of fixing a slow render.

Try typing rapidly in the input box and you’ll notice that there is a noticeable delay in the text being displayed as you type. As mentioned earlier, since the entire functional component is considered the render method, we see that the sleep function is being called on every render, causing a delay in the text input.

Let’s pretend that the sleep function is actually an expensive computation which returns a value we need in our Input component and that we don’t need to recompute this calculation on every render. We can avoid re-calling our sleep function on every render by using useMemo.

Simply change this line

const sleeping = sleep(sleepSeconds)

to

const sleeping = React.useMemo(() => sleep(sleepSeconds), [sleepSeconds]);

By doing so, we are memoizing the value returned by the sleep function. The sleep function will only ever get called again if the value in the dependency array — in this case [sleepSeconds]— ever changes. Now try typing in the input box below and you’ll see that there is no longer a delay while typing.

Making an edit based on a suggestion by Kent. Thanks Kent!

We should resort to using useMemo only after our best effort at making the slow function as fast as possible since we’ll likely end up having to re-render our component at some point anyway.

Fixing Unnecessary Renders using React.memo

React is extremely performant when it comes to re-rendering components. If it weren’t, it wouldn’t be a viable solution for large applications and it wouldn’t be as popular of a framework as it is today. However, re-rendering can still become a performance problem when it happens too frequently on some components.

You should only fix unnecessary renders using React.memo if there is a perceived or measurable performance issue. A common problem you’ll find in React applications is findingReact.memo(or the class component alternative of PureComponent) used all over the app since it’s a seemingly easy way to fix performance issues. Using React.memo correctly is often a multi-step process that increases the overall complexity of the app, so it should only be used as a last resort.

Let’s take another look at our pokemon menu example.

Remember that there was a delay when typing in the search input since we are re-rendering all 807 pokemon menu items. Now let’s pretend that there is a business requirement such that we need to keep the search input state at the parent level, so we can’t extract it out to the SearchInput component as we did earlier. (This is a common requirement that the state needs to stay at the parent level because either the parent needs to display the state or it needs to have the state in order to pass it to other child components.)

Since we can’t extract a component for the inputValue state, let’s attempt to fix the delay using React.memo instead and we’ll find that it doesn’t always fix the issue on the first implementation.

In the above example, we wrapped MenuItem in React.memo. This results in the component only re-rendering when the props change. MenuItem has only one prop of onClick that takes a handleClose function. Even though our handleClose function doesn’t seem to change, when typing in the search input, we are still re-rendering the 807 Pokemon menu items.

Why is this still happening? On every re-render of the top level App component we are redefining

const handleClose = () => {
setAnchorEl(null);
};

since, when using React hooks, everything within the App function is considered part of the render function. In addition, () => {} === () => {} returns false in Javascript — this means our handleClose function will never have referential equality, causing the MenuItem to re-render even if it was wrapped in React.memo.

To prevent the MenuItem from re-rendering when using React.memo, we have to take another step. React provides a function called useCallback to fix this exact issue.

We can change our handleClose function above to use useCallback:

const handleClose = React.useCallback(() => {
setAnchorEl(null);
}, []);

Here is the definition of useCallback from the React docs:

useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders

Now that we have a memoized version of handleClose, our React.memo finally works! MenuItem no longer re-renders when typing in the search input and there are no delays when typing. Here is the final solution:

In addition to () => {} === () => {} returning false in Javascript, [] === [], and {} === {} also return false. This means that whenever we pass arrays or objects to a component using React.memo, we have to make sure to memoize the array or object using useMemo in order to have referential equality, and for React.memo to work as we expect. Since it’s a very common occurrence to pass functions, arrays, or objects to components, it’s likely that you’ll have to pair React.memo with useCallback and/or useMemo.

As demonstrated, using React.memo is not as simple as it seems. It increases the overall complexity of your components since you have to make sure that the props you are passing have referential equality by wrapping them inuseCallback or useMemo.

For this reason, we should only take this approach if we have to, and we shouldn’t prematurely wrap our components in React.memo because we think it will make things faster.

Resources

Check out these blog posts for more detailed explanations on some of the topics mentioned above:

Special thanks to Kent C. Dodds since most of this blog post was derived from attending his React Performance workshop and from reading his blog posts.

We’re hiring!

--

--