On this page
A component rerender is not automatically a performance problem. React may call a component without changing the DOM, and a cheap render may cost less than the comparison introduced to avoid it. Optimization starts with an interaction that feels slow and evidence about where its time goes.
Measure one real interaction
Choose a concrete action such as typing in a filter, selecting a row, or opening a settings panel. Record it with React DevTools Profiler and the browser performance tools. Measured cost is more useful than the total number of console logs printed during development.
If large props obscure the investigation, inspect a sanitized sample in the JSON Formatter. Identify the fields the expensive child actually needs. Passing a complete account record when the child only needs its name makes identity changes harder to reason about and broadens the component contract unnecessarily.
Distinguish rendering work from network latency, layout, and event-handler computation. useMemo cannot accelerate a slow request or prevent expensive browser layout caused by DOM changes. A performance recording helps determine whether the bottleneck is inside React before you redesign component props.
Understand the three different tools
memo can let React skip a component rerender when its props compare equal. useMemo caches a calculation result between renders while its dependencies remain equal. useCallback caches a function identity; it does not make the function's body run faster when the function is called.
These tools depend on reference stability. A freshly created array, object, or callback is a different value under Object.is even when its contents appear equivalent. A memoized child receiving a new onSelect function on every parent render may not benefit from memo at all.
import { memo, useCallback, useMemo, useState } from 'react';
type Item = { id: string; name: string };
const Results = memo(function Results({
items, onSelect,
}: {
items: Item[];
onSelect: (id: string) => void;
}) {
return <ul>{items.map(item => (
<li key={item.id}>
<button onClick={() => onSelect(item.id)}>{item.name}</button>
</li>
))}</ul>;
});
export function Search({ items }: { items: Item[] }) {
const [query, setQuery] = useState('');
const [selected, setSelected] = useState<string | null>(null);
const visible = useMemo(() => items.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
), [items, query]);
const choose = useCallback((id: string) => setSelected(id), []);
return <>
<input aria-label="Search" value={query}
onChange={event => setQuery(event.target.value)} />
<Results items={visible} onSelect={choose} />
<output>{selected}</output>
</>;
}The example demonstrates relationships, not a claim that filtering a short list needs memoization. On every query change, visible legitimately changes. The benefit appears when an unrelated parent update would otherwise recreate the filtered array and rerender an expensive child. Useful reuse requires a real unchanged case.
Fix state placement before adding caches
If a hover flag stored at the page root rerenders a large editor, move it closer to the component that owns the hover interaction. If a provider value changes for every keystroke, split unrelated concerns. Memoization can reduce symptoms while leaving the unnecessarily broad update path intact.
Composition also helps. A stateful wrapper can accept children that are created by a parent rather than recreating an entire subtree inside its own render. This is not a magical no-render guarantee, but it can make the intended ownership clearer and reduce avoidable work.
Read the React memo reference before writing a custom comparison function. A comparator must account for every prop that influences output or behavior, including functions that close over state. Returning true while ignoring a changed callback can preserve stale behavior.
Prefer narrow props over deep comparison. Passing selectedId and title may be clearer than passing a mutable configuration tree and asking a comparator to inspect it. Deep comparisons can become more expensive than rendering, especially as data structures grow or gain new nested fields.
Keep memoization out of correctness logic
A useMemo value is a performance optimization, not a permanent storage guarantee. Do not depend on it to allocate a resource that must be created exactly once, preserve a business invariant, or perform a side effect. React may discard cached calculations under documented circumstances.
The useMemo reference emphasizes purity. Calculations should return values without mutating props or performing network requests. Development checks can call calculations more than once to reveal impurity. Code that only works because a cache happens to persist has the wrong ownership model.
Likewise, useCallback requires complete dependencies. A stable callback that reads an old selected record is worse than a fresh callback that behaves correctly. Functional state updaters can remove dependencies when the next value depends only on the previous state, but do not hide other reactive reads.
The useCallback reference explains the dependency contract. Correct closures come before stable identities. If the project uses React Compiler, follow its integration guidance and inspect actual output and profiler behavior rather than assuming every manual memo is still necessary or every one is redundant.
Verify the improvement and maintenance cost
Record the same interaction before and after the change under comparable conditions. Include realistic data volume and representative hardware when possible. Development Strict Mode and development tooling can alter observations, so verify important findings in an appropriate profiling setup.
Check whether the optimization moved cost rather than removing it. A large dependency comparison or retained array can consume memory and time. Net improvement includes those costs as well as the skipped render. Remove an optimization that adds complexity without a measurable benefit.
Add behavior tests around selection and filters so future dependency changes do not introduce stale callbacks. A rendering optimization should preserve every user-visible result. Test that deleting an item, changing the source data, and replacing a callback still update the interface correctly.
Use the React collection for adjacent patterns and the development collection for broader implementation references. Document the specific interaction that justified the optimization near unusual memoization code. That gives future maintainers a reproducible reason to preserve or remove it.
Finally, avoid optimizing every leaf preemptively. Selective optimization keeps component APIs understandable and leaves profiling evidence useful. When all components contain manual caches by default, identifying the few calculations that actually matter becomes harder.
Compare representative prop changes
For a slow results list, profile three interactions separately: typing a search query, toggling an unrelated sidebar, and selecting a result. Memoization may correctly skip the list during the sidebar toggle while still rendering it for a changed query. That is a successful boundary, not evidence that the cache failed whenever the list renders.
Record a small set of repeat runs using the same dataset and build configuration. Compare commit duration and interaction responsiveness rather than choosing the single fastest trace. Development checks and browser extensions can distort measurements, so confirm the result in a representative profiling environment before making a performance claim.
Then deliberately change every prop that influences visible output. A custom comparison that ignores a callback can retain a handler closing over old state even when the screen looks unchanged. Click the result after changing its permissions or selection context and verify the handler observes current values. Correctness checks belong beside the timing comparison, not after the optimization ships.
Key takeaways
- Profile a real slow interaction before treating rerender counts as a problem.
- Use memo for component props, useMemo for calculations, and useCallback for function identity.
- Prefer local state and narrow contracts, and never make correctness depend on a memoized cache.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor