React performance optimization involves preventing unnecessary re-renders with memoization, virtualizing long lists, splitting context providers, and profiling with React DevTools to identify actual bottlenecks before optimizing.
Skips re-rendering a component when its props are shallowly equal. Only beneficial for expensive components that frequently receive unchanged props.
useMemo caches computed values, useCallback caches function references. Both prevent unnecessary work when dependencies have not changed.
Renders only visible rows plus a buffer instead of the full list. Libraries like react-virtual make 10,000-item lists perform like 20-item lists.
Split contexts by update frequency so fast-changing values do not trigger re-renders in components that only consume slow-changing values.
DevTools Profiler shows which components rendered, why, and for how long. Always measure before optimizing to find actual bottlenecks.
React's reconciliation algorithm is fast, but unnecessary re-renders can accumulate and cause noticeable lag in complex applications. The key principle is to measure first, then optimize — premature optimization is the root of all evil, and React is already highly optimized out of the box.
By default, when a parent re-renders, all its children re-render too — even if their props have not changed. React.memo wraps a component and skips re-rendering when its props are shallowly equal to the previous props:
const ExpensiveList = React.memo(({ items }) => {
return items.map(item => <ExpensiveItem key={item.id} item={item} />);
});Be cautious: React.memo adds overhead for the shallow comparison. Only use it when the component is actually expensive to render and frequently receives the same props. For simple components, the comparison cost can exceed the render cost.
useMemo memoizes the result of a computation, recalculating only when dependencies change. useCallback memoizes a function reference, preventing it from being recreated on every render. Both are primarily useful when passing values or callbacks to memoized child components:
const sortedItems = useMemo(() =>
items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
const handleClick = useCallback((id) => setSelected(id), []);Without useCallback, a new function reference is created every render, defeating React.memo on the child. Without useMemo, expensive computations repeat unnecessarily. However, using these hooks everywhere adds complexity and memory overhead for the cached values — only apply them where profiling shows a real benefit.
Rendering thousands of DOM nodes (e.g., a 10,000-row table) is slow regardless of React optimizations. Virtualization renders only the visible rows plus a small buffer, drastically reducing DOM nodes. Libraries like @tanstack/react-virtual, react-window, and react-virtuoso implement this pattern. A virtualized list of 10,000 items renders as fast as one with 20 items because only ~20 DOM nodes exist at any time.
When a context value changes, every component consuming that context re-renders. This becomes a problem when a single context holds many values that change at different frequencies. The solution is to split contexts by update frequency: fast-changing values (mouse position, form input) in one context, slow-changing values (user settings, theme) in another. Alternatively, use useSyncExternalStore or state management libraries (Zustand, Jotai) that support fine-grained subscriptions.
The React DevTools Profiler records component renders and shows exactly which components rendered, why they rendered, and how long each render took. The Highlight Updates feature visually outlines components as they re-render, making unnecessary renders immediately visible. Always profile before optimizing — the bottleneck is rarely where you expect.
React Compiler (previously React Forget) is an experimental build-time tool that automatically inserts memoization. It analyzes your components and adds useMemo, useCallback, and React.memo equivalents where beneficial, eliminating the need to manually optimize most re-render patterns. When mature, it will make manual memoization largely unnecessary.
Move state down — keep state as close to where it is used as possible, so updates only re-render the relevant subtree. Lift content up — pass children as props to avoid re-rendering static content when parent state changes. Use composition over context for passing data through component trees.
React.memo prevents re-renders when props have not changed. useMemo caches computed values. useCallback caches function references. Virtualization reduces DOM nodes. Context splitting prevents unrelated re-renders. The React Profiler identifies which of these optimizations is actually needed — always measure before optimizing.
Fun Fact
React's virtual DOM diffing was revolutionary when introduced in 2013, but the React team has since said that the virtual DOM is more of an implementation detail than a feature. The real innovation was the declarative component model — the virtual DOM was just an efficient way to make it work. React Compiler takes this further by optimizing at build time.