React Performance Optimization Guide | useMemo, useCallback & Profiling
이 글의 핵심
Practical rules for React performance: cache expensive work and stabilize references when memoized children need it—measure with Profiler before adding hooks.
Introduction
In React 18/19, function components re-run whenever props, state, or a parent render changes. useMemo and useCallback reuse previous results to cut unnecessary work and reference churn. Decide when to use React useMemo and useCallback by asking: Is this computation expensive? and Does a child or hook need a stable reference? Both hooks add overhead—misuse can slow the app.
This article covers when memoization pays off and how to validate with the Profiler. For async flows see the async guide; for debugging see async debugging case study.
Table of contents
- Concepts
- Step-by-step examples
- Advanced: memo and Context
- Performance comparison
- Real-world scenarios
- Troubleshooting
- Conclusion
Concepts
- Re-render: When a parent renders, children usually render again unless you optimize.
- Referential equality: Object, array, and function literals create new references every render—bad for
React.memochildren and someuseEffectdependency arrays. useMemo: Recomputes a value only when dependencies change.useCallback: Keeps a stable function reference when dependencies change—syntactic sugar overuseMemo(() => fn, deps).
Step-by-step examples
1) Heavy list filtering — useMemo
import { useMemo, useState } from "react";
type Item = { id: string; label: string; score: number };
export function Leaderboard({ items }: { items: Item[] }) {
const [query, setQuery] = useState("");
const visible = useMemo(() => {
const q = query.trim().toLowerCase();
if (!q) return items;
return items.filter((it) => it.label.toLowerCase().includes(q));
}, [items, query]);
return (
<section>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ul>
{visible.map((it) => (
<li key={it.id}>
{it.label} — {it.score}
</li>
))}
</ul>
</section>
);
}
With thousands of rows or a costly filter, gains are easier to measure.
2) Stable child callbacks — useCallback + memo
import { memo, useCallback, useState } from "react";
const Row = memo(function Row({
id,
onSelect,
}: {
id: string;
onSelect: (id: string) => void;
}) {
return <button onClick={() => onSelect(id)}>{id}</button>;
});
export function Table({ ids }: { ids: string[] }) {
const [active, setActive] = useState<string | null>(null);
const handleSelect = useCallback((id: string) => {
setActive(id);
}, []);
return (
<div>
<p>active: {active}</p>
{ids.map((id) => (
<Row key={id} id={id} onSelect={handleSelect} />
))}
</div>
);
}
If handleSelect were recreated every render, Row’s memo would be defeated.
3) Split Context values
Putting { value: { a, b } } in Context with a new object each render updates every consumer. Split fast-changing and slow-changing data, or wrap the value in useMemo.
Advanced: memo and Context
memo(Component, arePropsEqual): Use custom comparison only when shallow compare is wrong—too much logic hurts maintainability.- React Compiler: As the ecosystem adopts automatic memoization, manual hooks may matter less—until then, measure first.
- Server Components: Fetching on the server can reduce client memoization—architecture comes first.
Performance comparison
| Situation | useMemo | useCallback |
|---|---|---|
| Heavy pure computation | Candidate | N/A |
| Stable object/array props | Candidate | Sometimes pair with useMemo for objects |
Handler passed to memo children | Small direct gain | Meaningful with memo |
| Cheap work | Can add overhead | Same |
Use React DevTools Profiler to compare commit time and render counts before and after changes.
Real-world scenarios
- Charts, tables, virtualized lists: Memoize expensive derived data when inputs rarely change.
- Deep trees with memoized middle layers:
useCallbackkeeps stable props for memoized children. - Library hooks: Some animation or chart hooks are sensitive to callback identity.
Troubleshooting
Child still re-renders after memo
→ Check for new object/function props from parents or Context values recreated each render.
useMemo not updating
→ A dependency may be referentially unstable (new object each render).
More complexity, no measurable win
→ Do not add hooks until Profiler proves a bottleneck.
Odd behavior with Concurrent features
→ Keep render pure; put side effects in useEffect / useLayoutEffect.
Conclusion
Think of useMemo and useCallback along two axes: reduce render work and stabilize references. Default to Profiler-driven changes, and guard against over-memoization in code review. For broader patterns, see JavaScript patterns.