My React App is Slow. What Should I do?
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 Menu
component 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:
- State Colocation - https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster
- useMemo, useCallback, and referential equality - https://kentcdodds.com/blog/usememo-and-usecallback
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!