TL;DR
- I profile first and optimize second.
- The biggest wins usually come from reducing unnecessary work, not from clever micro-optimizations.
React.memo,useMemo, anduseCallbackonly help when they protect a real expensive boundary or stabilize a meaningful contract.- Long lists, chatty context providers, and expensive render paths usually cause more pain than React itself.
Context
When a React app feels slow, my first instinct is not to add memoization everywhere. I want to know whether the problem is too many renders, too much work per render, too much DOM on screen, or too much work happening on the main thread during input.
The Approach
My usual checklist:
- use the React Profiler
- find repeated renders
- find expensive renders
- check state placement
- virtualize large lists when needed
const Row = React.memo(function Row({ item }) {
return <div>{item.name}</div>
})
I only keep React.memo if:
- props are stable enough for it to matter
- the child is expensive enough to justify the comparison
If the issue is a long list, virtualization is usually a bigger win than hook-level memo tuning.
I also watch for these recurring patterns:
- context providers that change value every render
- inline object or function props crossing many memo boundaries
- expensive filtering or sorting inside render
- synchronous work inside input handlers that should be deferred or moved
If an interaction-heavy view still struggles after structural cleanup, I start thinking about:
useTransitionuseDeferredValue- web workers for heavy computation
- smarter server/client boundary choices
Trade-offs
- Memoization can help, but it adds cognitive load and sometimes hides the real architecture problem.
- Virtualization improves throughput, but it makes layout and measurement more complex.
- Splitting context reduces noisy re-renders, but it can make the provider tree more verbose.
- Deferred rendering patterns help responsiveness, but they also make update timing less immediate and sometimes harder to debug.