React Performance Optimization Guide | useMemo, useCallback & Profiling

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

  1. Concepts
  2. Step-by-step examples
  3. Advanced: memo and Context
  4. Performance comparison
  5. Real-world scenarios
  6. Troubleshooting
  7. 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.memo children and some useEffect dependency 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

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

SituationuseMemouseCallback
Heavy pure computationCandidateN/A
Stable object/array propsCandidateSometimes pair with useMemo for objects
Handler passed to memo childrenSmall direct gainMeaningful with memo
Cheap workCan add overheadSame

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: useCallback keeps 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.