[2026] React Performance Optimization Complete Guide | useMemo, useCallback & Profiling
이 글의 핵심
React performance optimization: when useMemo and useCallback help, when they hurt, memo with stable props, Context splitting, and React DevTools Profiler—avoid premature optimization.
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 over
useMemo(() => fn, deps).
Step-by-step examples
1) Heavy list filtering — useMemo
다음은 tsx를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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
다음은 tsx를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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.