React useMemo and useCallback: When They Pay Off | Render Optimization
이 글의 핵심
Use useMemo and useCallback for stable references and heavy pure work—establish proof with Profiler before sprinkling them everywhere.
Introduction
In React 18/19, function components re-run on props, state, and parent re-renders. useMemo and useCallback reuse previous results to cut unnecessary work and reference churn. Deciding when to use React useMemo and useCallback starts with: “Is this expensive pure work?” vs “Do I need reference stability?”—but both hooks have their own cost, so overuse can slow things down.
This post covers criteria for when memoization helps 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 usage
- Performance comparison
- Real-world cases
- Troubleshooting
- Wrap-up
Concepts
- Re-render: by default, when the parent renders, children render again too (unless conditional).
- Reference identity: object, function, and array literals get new references every render—bad for
React.memochildren oruseEffectdependency arrays. useMemo: recompute a value only when dependencies change.useCallback: keep a function reference stable unless 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 items or a heavy filter, gains are easier to measure.
2) Stabilize 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 is recreated every render, Row’s memo is weakened.
3) Split Context values — avoid one big object in Provider
Putting { value: { a, b } } with a new object every render refreshes all consumers. Split fast-changing vs slow-changing values or wrap value in useMemo.
Advanced: child memo and custom compare
memo(Component, arePropsEqual): use custom comparison only when shallow compare is insufficient—too much custom logic hurts debugging.- React Compiler (ecosystem): automatic memoization may reduce manual hooks—until then, measure first.
- Server Components: fetching on the server reduces client memoization load—boundary design comes first.
Performance: when it pays off
| Situation | useMemo | useCallback |
|---|---|---|
| Heavy pure computation | candidate | N/A |
| Stable object/array props | candidate | sometimes both |
Handler passed to memo children | small direct gain | pairs with child memo |
| Already cheap work | may add overhead only | same |
Compare commit time and render counts in React DevTools Profiler before and after.
Real-world cases
- Charts, tables, virtual scroll:
useMemowhen data shaping is expensive. - Handlers through deep trees:
useCallbackwhen intermediate components usememo. - Library hooks: some animations/charts need stable callbacks.
Troubleshooting
Memoized but child still renders every time
→ Check new object/function props from parents or a Context value recreated every render.
useMemo not updating
→ Dependencies include a unstable reference.
More complex code, no felt benefit
→ Default rule: do not add until Profiler proves a bottleneck.
Weirdness under Concurrent rendering
→ Keep render pure; side effects belong in useEffect / useLayoutEffect.
Wrap-up
Treat useMemo and useCallback as “reduce render cost” and “stabilize references.” Default to Profiler first, then apply; guard against team-wide over-memoization. Pair with JavaScript patterns for component design.