Stockio

useMemo vs useCallback vs React.memo: Fewer Rerenders

Learn when React memoization helps, how prop identity affects rerenders, and why profiling and state placement should come before useMemo and useCallback.

By Daniel Kim

React Developer ·

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.
Featured sponsor

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

Keep reading

Related articles

More in JavaScript & Frameworks
JavaScript & Frameworks

10 Best JavaScript Charting Libraries

Compare ten JavaScript chart libraries for dashboards, reports, and interactive data stories. See where each excels, what it costs, and how to choose based on your app and team.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

8 Best React State Management Libraries

Find a React state approach that fits your app, from built-in hooks to dedicated stores, with clear trade-offs around data flow, debugging, async work, and learning effort.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

12 Best and Most Popular JavaScript Libraries

Compare twelve widely used JavaScript libraries for interfaces, data, testing, and everyday development, with clear strengths, trade-offs, and advice for choosing only what your project needs.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

localStorage vs sessionStorage vs IndexedDB Compared

Choose browser storage by data lifetime, size and access pattern. Compare synchronous Web Storage with IndexedDB transactions, migrations, quotas and failure handling.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

React Server Components Explained for Developers

Learn what React Server Components execute, what crosses the client boundary, and how to structure data access, interactive widgets, and mutations without leaking private data.

Includes: JSON formatterDaniel KimDaniel Kim
JavaScript & Frameworks

Deep Copy in JavaScript: structuredClone vs Spread

Understand when spread is enough, when structuredClone creates a useful snapshot, and why targeted immutable updates often beat cloning an entire object graph.

Includes: JSON formatterDaniel KimDaniel Kim