Three primitives form the foundation of render performance work. useMemo(factory, deps) memoizes the result of factory and recomputes only when a dep changes. useCallback(fn, deps) is shorthand for useMemo(() => fn, deps) — it memoizes the function itself. React.memo(Component) skips a re-render of the wrapped component when its props are referentially equal. These three are often paired: a parent uses useCallback to stabilize a handler, the child is wrapped in React.memo to benefit, and useMemo stabilizes object or array props that would otherwise be new identities each render. Object literals in JSX — for example, <Row style={{ margin: 10 }} /> — create new references every render and silently break memoization. Always pass useMemo-stabilized values or hoist constants outside the component.
Memoization is not free. Every memo adds bookkeeping: dep comparison, cache lookup, more closure allocations. It pays off only when the work it avoids outweighs that overhead. Common signs that memoization is wasted include object deps that change every render, functions that never get passed to memoized children, and components whose render cost is dominated by DOM diffing rather than JS. The card on memo pitfalls is explicit: do not wrap tiny components, do not memoize when deps always change, and profile before optimizing. The React Profiler and the "why did this render?" hints in DevTools are the right way to confirm. A render-counter ref (const r = useRef(0); r.current++; console.log(...)) is a quick-and-dirty debugging aid for spotting extra renders during development.
Two big shifts are reshaping this landscape. First, the React Compiler (sometimes called "Forget") analyzes your code at build time and inserts memoization automatically, removing most manual useMemo and useCallback usage once it stabilizes — but you should still profile to confirm, and code written for the compiler reads better than code written for hand-rolled memoization. Second, list virtualization libraries like react-window keep the DOM bounded by rendering only visible rows, providing orders-of-magnitude wins for long lists that no amount of memoization can match. Underneath all of this, the key prop on lists should be a stable unique id, not the array index; index keys preserve state by position rather than by item, so reorders cause wrong state to land on the wrong row. Memoize rows, pass stable callbacks via useCallback, and key by stable id — that combination covers most list-performance work.