Optimize Rendering
Loading "Intro to Optimize Rendering"
Run locally for transcripts
Here's the lifecycle of a React app:
Let's define a few terms:
- The "render" phase: create React elements React.createElement
- The "reconciliation" phase: compare previous elements with the new ones
- The "commit" phase: update the DOM (if needed).
React exists in its current form (in large part) because updating the DOM is the
slowest part of this process. By separating us from the DOM, React can perform
the most surgically optimal updates to the DOM to speed things up for us
big-time.
A React Component can rerender for any of the following reasons:
- Its props change
- Its internal state changes
- It is consuming context values which have changed
- Its parent rerenders
React is really fast, however, sometimes it can be useful to give React little
tips about certain parts of the React tree when there's a state update. You can
opt-out of state updates for a part of the React tree by using
memo
.I want to emphasize that I've seen many projects make the mistake of using these
utilities as band-aids over more problematic performance problems in their apps.
Please read more about this in my blog post:
Fix the slow render before you fix the rerender.
Let's look at an example to learn how this works. You can pull this example up
at /app/example.unnecessary-rerenders
(and feel free to play with it in
examples/unnecessary-rerenders
). Pull this
up and profile it with the React DevTools.Here's the implementation:
function CountButton({
count,
onClick,
}: {
count: number
onClick: () => void
}) {
return <button onClick={onClick}>{count}</button>
}
function NameInput({
name,
onNameChange,
}: {
name: string
onNameChange: (name: string) => void
}) {
return (
<label>
Name:{' '}
<input
value={name}
onChange={(e) => onNameChange(e.currentTarget.value)}
/>
</label>
)
}
function App() {
const [name, setName] = useState('')
const [count, setCount] = useState(0)
const increment = () => setCount((c) => c + 1)
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<div>
<CountButton count={count} onClick={increment} />
</div>
<div>
<NameInput name={name} onNameChange={setName} />
</div>
{name ? <div>{`${name}'s favorite number is ${count}`}</div> : null}
</div>
)
}
Based on how this is implemented, when you click on the counter button, the
<CountButton />
rerenders (so we can update the count
value). But the
<NameInput />
is also rerendered. If you have
Record why each component rendered while profiling.
enabled in React DevTools,
then you'll see that under "Why did this render?" it says "The parent component
rendered."React does this because it has no way of knowing whether the NameInput will need
to return different React elements based on the state change of its parent. In
our case there were no changes necessary, so React didn't bother updating the
DOM. This is what's called an "unnecessary rerender" and if that
render/reconciliation process is expensive, then it can be worthwhile to prevent
it.
Using one of the bail-out APIs, you can instruct React when to rerender.
The only relevant API these days is
memo
. What happens is that React will
compare the previous props with the new props and if they're the same, then
React will not call the component function and will not update the DOM.So here's how we can improve our example:
import { memo } from 'react'
function CountButton({
count,
onClick,
}: {
count: number
onClick: () => void
}) {
return <button onClick={onClick}>{count}</button>
}
const NameInput = memo(function NameInput({
name,
onNameChange,
}: {
name: string
onNameChange: (name: string) => void
}) {
return (
<label>
Name:{' '}
<input
value={name}
onChange={(e) => onNameChange(e.currentTarget.value)}
/>
</label>
)
})
// etc... no other changes necessary
The only change there was to wrap the
NameInput
component in react's memo
utility.If you try that out, then you'll notice the
<NameInput />
no longer rerenders
when you click on the counter button, saving React the work of having to call
the NameInput
function and compare the previous react elements with the new
ones.Again, I want to mention that people can make the mistake of wrapping
everything in
memo
which can actually slow down your app in some cases and
in all cases it makes your code more complex.In fact, why don't you try wrapping the
CountButton
in memo
and see what
happens. You'll notice that it doesn't work.This is because its parent is passing
increment
and that function is new every
render (as it's defined in the App
). Because memo
relies on the same props
each call to prevent unnecessary renders, we're not getting any benefit! If we
really wanted to take advantage of memo
here, we'd have to wrap increment
in
useCallback
. Feel free to try that if you'd like.It's much better to use
memo
more intentionally and further, there are other
things you can do to reduce the amount of unnecessary rerenders throughout your
application (some of which we've done already).