React.memo skips re-renders when props haven't changed, useMemo caches computed values, and useCallback caches function references — but these are targeted optimizations, not defaults for every component.
HOC that skips re-renders when props haven't changed (shallow comparison). Only effective when all props are referentially stable.
useMemo caches computed values, useCallback caches function references — both keep prop references stable for memoized children.
Use React DevTools Profiler to identify actual bottlenecks. Memoization adds complexity — don't apply it everywhere by default.
React.memo on the child + useMemo/useCallback on the props passed to it = effective memoization. One without the other often has no effect.
React 19's compiler auto-memoizes at build time — may eliminate the need for manual React.memo, useMemo, and useCallback.
React re-renders a component whenever its state changes or its parent re-renders. Most of the time this is fast enough, but for expensive components or large lists, unnecessary re-renders cause visible lag. React provides three memoization tools to address this.
A higher-order component that wraps a functional component and skips re-rendering when all props are the same (shallow comparison). const MemoizedList = React.memo(List) — if the parent re-renders but List's props haven't changed, React reuses the previous output without calling the component function.
Critical caveat: React.memo only works if all props are referentially stable. If the parent creates new objects or functions on each render (<List items={data.filter(x => x.active)} onClick={() => handleClick()} />), the props are different every time and memo has no effect. This is where useMemo and useCallback become relevant.
const filtered = useMemo(() => data.filter(x => x.active), [data]) caches the computed value between renders, only recomputing when dependencies change. Without useMemo, the filter runs on every render and produces a new array reference, breaking React.memo.
Use cases: expensive computations (sorting, filtering large lists), creating objects/arrays passed to memoized children, complex derived values.
const handleClick = useCallback(() => { ... }, [deps]) caches the function reference between renders. It's equivalent to useMemo(() => fn, [deps]). Without it, () => { ... } creates a new function on every render, breaking React.memo on children that receive it as a prop.
Use cases: event handlers passed to memoized children, functions used in useEffect dependency arrays, callbacks passed to expensive child components.
Memoization adds complexity and memory overhead. Don't use it unless you've identified a performance problem:
a + b) — the memoization overhead exceeds the computation costThe Profiler tab in React DevTools records rendering performance. It shows which components rendered, how long each took, and why they re-rendered (props changed, state changed, parent rendered). This is the essential tool for identifying optimization targets.
The React Compiler automatically memoizes components and values at build time, potentially eliminating the need for manual React.memo, useMemo, and useCallback. As of 2025, it's production-ready at Meta and being adopted by the wider ecosystem. It analyzes component code and inserts memoization where beneficial.
React.memo prevents child re-renders when props are unchanged. useMemo caches values to keep prop references stable. useCallback caches functions for the same purpose. These three work together — memo on the child, useMemo/useCallback on the props. But don't optimize blindly — profile first, optimize specific bottlenecks, and know that the React Compiler may automate this entirely.
Fun Fact
Dan Abramov (React core team) famously wrote 'Before You memo()' arguing that restructuring components (moving state down, lifting content up) is often more effective than memoization. The React Compiler embodies this philosophy — it analyzes your code structure and applies optimizations automatically.