Optimizing React Renders and Avoiding Unnecessary Re-Renders
One of the most annoying parts of React (IMHO), and often (oddly) left out of performance discussions around React...
What Causes a React Component To Re-Render
First, we should clarify what causes a React component to re-render, and the answer is pretty much... anything. Well, not exactly, but close:
- An update to the state of that component. This includes changes to:
props
(regardless of actual usage)- values derived from
props
- The actual explicit state values (e.g. via
setState
)
- And / or, ANY of the component's parents re-rendering
- This is a very common annoyance - if your component re-renders, it blindly re-renders all of its children as well!
Unfortunately, the official React docs don't do the best job of specifying this behavior, and the two best resources I have found for explaining what causes re-renders are from external sources:
💡 / ⚠ - An easy-to-miss missed thing about prop comparison: Passing an anonymous function (aka inline function / callback) as a prop will break prop-comparison in things like
React.memo
andPureComponent
; if the parent changes, even if the anonymous function stays the same, React will always infer that the props have changed, since it has no way to compare the old anonymous function to the new one. This has been written about in many places, including here, here, and here. TheuseCallback
hook is a good way to address this, but comes with its own caveats (ref A, ref B)
⚠ There is a lot of misleading information out there about the Virtual DOM (aka VDOM) and its benefits. People tend to incorrectly believe that React's dom-diffing means that re-renders are avoided if nothing has changed, but this is only partially true; yes, React will reduce the number of real DOM updates if nothing has changed, but React treats VDOM re-renders as inexpensive, and is part of why it lets almost anything trigger a VDOM re-render, which eventually has to be diffed against the real DOM as well. For the most part, this can stay fast because of how VDOM works, but this can also be a big problem if you have a lot of (processor heavy) logic tied to the rendering of your components.
This post from Rich Harris, "Virtual DOM is pure overhead", is a great related read.
Tracing a Re-Render
Even knowing all the above, about what causes a React re-render, it might still be hard to pin down what exactly is causing a specific component (or component tree) to re-render. Thankfully, there are tools you can use to help track down what is triggering a re-render:
- React Dev Tools - The React Profiler
- The React Profiler API
- Browser stack traces / Performance monitoring
- Adding console output. E.g.:
console.trace()
- Hooking into the callbacks / comparison methods of
React.memo
orshouldComponentUpdate()
- Adding a
useEffect()
hook to listen for changes on specific props / state values and specifically log them
This post does a great job of covering some of the best approaches for debugging excessive rerenders in React - brycedooley.com/debug-react-rerenders.
Avoiding Re-Renders
The first step to avoiding a re-render might be to simply understand why it is happening. See the above section(s) for the general reasons why a React component gets re-rendered, and how to trace the specific changes triggering yours. Regardless of the reason, there are a few general approaches for reducing unnecessary components and optimizing performance.
- For Class-Based Components
- Hook into the
shouldComponentUpdate
lifecycle method- You basically override it based on your set of criteria for why your component should or should not update on changed props
- Here are some examples from the React docs on using this to optimize performance
- Make sure to heed the warnings listed in the docs
- Inherit your component from
React.PureComponent
class (by extending the class / subclassing it)- This basically has a predefined
shouldComponentUpdate
that shallow compares old props and state to new props and state, and prevents an update if they are the same - The examples on the React performance page also discusses the difference between using
PureComponent
versus hand-codingshouldComponentUpdate
.
- This basically has a predefined
- Hook into the
- For Function-Based Components (e.g. a React Hooks approach)
- Memoization is the main tool you have available when it comes to preventing re-renders with hooks
- For entire components:
React.memo(MyComponent)
HOC- Warning: This only checks for unchanging props; any calls to
useState
oruseContext
inside the component will still trigger a re-render. - This also has issues with {children} passed via props
- You can pass a custom prop comparison method as the second argument
- Warning: This only checks for unchanging props; any calls to
- For any value: The
useMemo()
hook
- For entire components:
- Here is a guide on optimizing with hooks.
- Memoization is the main tool you have available when it comes to preventing re-renders with hooks
📘 This post is a good alternative summary of the above options.
There is some truth to the "avoid premature-optimization" argument. In general, React re-rendering in the VDOM is very fast, and you don't need to optimize it. Adding methods that check props and state and do deep comparisons to reduce re-renders actually adds overhead. For example, it would be a bad situation if over a span of time, you reduced renders from 300 to 290, but because you did so with a deep prop equality check, you added 300 NEW comparison calls. This infographic on when to use `React.memo() is a good breakdown of reasons to go down this route.