Optimize Rendering

Here's the lifecycle of a React app:
โ†’ render โ†’ reconciliation โ†’ commit โ†’ state change โ†’ rerender
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:
  1. Its props change
  2. Its internal state changes
  3. It is consuming context values which have changed
  4. 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).