Suggest an editImprove this articleRefine the answer for “React performance optimization techniques”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**React performance optimization** is a set of techniques that prevent unnecessary re-renders, cache expensive calculations, and reduce bundle size in large apps. ```tsx const Child = React.memo(({ count }: { count: number }) => <div>{count}</div>); const stats = useMemo(() => computeStats(salesData), [salesData]); const handleExport = useCallback(() => exportToCSV(stats), [stats]); ``` **Key:** profile with React DevTools Profiler first, then apply `React.memo`, `useMemo`, virtualization, or code splitting only where the data shows an actual bottleneck.Shown above the full answer for quick recall.Answer (EN)Image**React performance optimization** is a set of targeted techniques (memoization, code splitting, virtualization) that prevent unnecessary re-renders, skip expensive recalculations, and reduce initial bundle size in large apps. ## Theory ### TL;DR - React re-renders every child when a parent's state changes; optimization adds a comparison gate to skip unchanged ones - `React.memo` caches a component's output; `useMemo` caches a computed value; `useCallback` caches a function reference - Profile first with React DevTools Profiler. State colocation and code splitting usually give bigger wins than adding `memo` everywhere - List with 200+ items → virtualization. Bundle over 500KB → `React.lazy`. Handler prop causing child re-renders → `useCallback` + `React.memo` ### Quick example ```tsx // Without optimization: Child re-renders on every count change function Parent() { const [count, setCount] = useState(0); return <Child name="Item" count={count} />; } // With React.memo: Child skips re-render if props are unchanged (shallow compare) const Child = React.memo(({ name, count }: { name: string; count: number }) => { console.log(`${name} rendered`); // Only logs when props actually change return <div>{name}: {count}</div>; }); ``` Parent re-renders on every button click. The memoized Child only logs when `name` or `count` actually changes. Without `React.memo`, it logs every single time. ### Why React re-renders by default React's reconciliation algorithm re-renders the entire subtree below any component whose state or props change. For small trees, that is fast enough to ignore. For a dashboard with 50+ child components, it adds up quickly. The optimization techniques do not change the algorithm. They add a shallow comparison gate before the component function runs. If props are the same reference or the same primitive value, React bails out before touching JSX or the DOM. ### When to use each technique - **Parent re-renders often, child props stay stable** → `React.memo` - **Expensive derived data: reduce, map, sort** → `useMemo` - **Handler passed as prop to a memoized child** → `useCallback` - **List with 200+ items** → `react-window` or `@tanstack/react-virtual` - **Initial bundle over 500KB** → `React.lazy` + `Suspense` - **State change re-renders distant parts of the tree** → state colocation ### Comparison table | Technique | What it caches | When it re-runs | Bundle impact | Available since | |-----------|---------------|-----------------|---------------|-----------------| | `React.memo` | Full component render | Prop change (shallow) | None | React 16.6 | | `useMemo` | Any computed value | Deps array change | None | React 16.8 | | `useCallback` | Function reference | Deps array change | None | React 16.8 | | `React.lazy` | Code chunk | First `import()` | Reduces initial | React 16.6 | | `react-window` | Visible list items | Scroll / viewport | ~10KB | Library | `useMemo` and `useCallback` are the same mechanism under the hood. `useCallback(fn, deps)` is shorthand for `useMemo(() => fn, deps)`. ### How React handles memoization internally React's fiber architecture batches state updates using a scheduler based on `requestIdleCallback`. During the commit phase, memoized components run a check similar to `shouldComponentUpdate`. It does a shallow object diff via an internal `areEqual` function. If every prop passes that check, React skips JSX traversal and DOM commits entirely. V8 also benefits here. Stable function references from `useCallback` let the engine reuse already-compiled closures instead of allocating new ones each render. ### Common mistakes **Mistake 1: Missing deps in `useMemo` or `useCallback`** ```tsx // Stale data: items changes but doubled never updates const doubled = useMemo(() => items.map(i => i * 2), []); // Fix: add the dependency const doubled = useMemo(() => items.map(i => i * 2), [items]); ``` The ESLint rule `react-hooks/exhaustive-deps` catches this automatically. It is the most common complaint on r/reactjs about hooks. **Mistake 2: Passing new objects or functions to a memoized child** ```tsx // Re-renders on every parent render because config is a new object each time const Child = React.memo(({ config }) => <div>{config.theme}</div>); <Child config={{ theme: "dark", size: 14 }} /> // ❌ // Fix: memoize the object, or pass primitives directly const config = useMemo(() => ({ theme: "dark", size: 14 }), []); <Child config={config} /> // ✅ ``` Shallow comparison checks references, not contents. A new object literal fails the check even if nothing inside changed. This accounts for a large share of StackOverflow questions about `React.memo` "not working." **Mistake 3: Using `useCallback` without memoizing the child** ```tsx // handleClick is stable, but Child still re-renders every time const handleClick = useCallback(() => console.log("click"), []); <Child onClick={handleClick} /> // Child has no React.memo ``` `useCallback` alone does nothing for re-render prevention. The child needs `React.memo` too, otherwise it ignores the stable reference entirely. **Mistake 4: Memoizing everything** Adding `React.memo` to every component adds comparison overhead on every render. For components that receive simple primitives or render rarely, that overhead can exceed the savings. Dan Abramov's blog shows profiler data with +15% CPU overhead from over-memoization. Apply memo only where the DevTools Profiler actually shows a problem. ### Real-world usage - **Next.js**: `React.lazy` / `Suspense` for route-level code splitting; `/dashboard` loads only when visited - **Redux Toolkit**: `createSelector` from Reselect wraps `useMemo` logic to cache derived state across the store - **Material-UI**: `React.memo` on `ListItem` to skip re-renders in drawers with 100+ items - **TanStack Query**: `useQuery` memoizes query data automatically so components only re-render on actual data changes - **Vercel commerce template**: `useMemo` for cart totals, `useCallback` for checkout handlers ### Follow-up questions **Q:** When does `React.memo` fail to prevent a re-render? **A:** When the component consumes a context that changed, or when a parent calls `forceUpdate`. Also when props contain objects or functions created inline, because shallow comparison sees them as new values every time. **Q:** What is the difference between `useMemo` and `useCallback`? **A:** `useMemo` caches any value: an object, array, or computed result. `useCallback` is a shortcut for `useMemo(() => fn, deps)`. Use `useCallback` when the cached value is a function, `useMemo` when it is data. **Q:** How do you optimize a list of 10,000 items? **A:** Virtualization with `react-window` or `@tanstack/react-virtual`. Only visible rows get rendered, roughly 9-15 items instead of 10,000. Memory drops by roughly 100x and scroll stays at 60fps where an unvirtualized list would fall to 15fps or less. **Q:** What is the difference between `useTransition` and `useMemo`? **A:** Different problems. `useMemo` skips recalculation. `useTransition` marks a state update as non-urgent so React can interrupt it for more urgent user input. Use both together in search-as-you-type: `useMemo` for filtering logic, `useTransition` to keep the input field responsive. **Q:** (Senior) In Concurrent Mode, how does the scheduler interact with memoized components? **A:** Low-priority updates started with `useTransition` can be interrupted mid-tree. React may skip rendering memoized components if a higher-priority update arrives. The memo check still runs, but the component might re-render anyway when the scheduler resumes the interrupted work. Measure impact via `getCurrentPriorityLevel()` from the React scheduler package. ## Examples ### Dashboard with useMemo and useCallback A sales dashboard that recalculates totals only when data changes and passes a stable export handler to a child toolbar. ```tsx function SalesDashboard({ salesData }: { salesData: Sale[] }) { // Recalculates only when salesData reference changes const stats = useMemo(() => { const total = salesData.reduce((sum, s) => sum + s.amount, 0); return { total, avg: total / salesData.length, count: salesData.length, }; }, [salesData]); // Stable reference: StatsPanel skips re-render when parent re-renders const handleExport = useCallback(() => { exportToCSV(stats); }, [stats]); return ( <div> <StatsPanel stats={stats} onExport={handleExport} /> <SalesTable data={salesData} /> </div> ); } ``` Without `useMemo`, `stats` recomputes on every parent render even when `salesData` has not changed. In a real dashboard with animations running and polling every 30 seconds, that adds up to hundreds of wasted recalculations per session. ### Virtualized list with react-window Rendering 10,000 product items without freezing the browser. ```tsx import { FixedSizeList as List } from "react-window"; const VirtualizedList = React.memo(({ items }: { items: Item[] }) => ( <List height={600} // Fixed container height in px itemCount={items.length} itemSize={70} // Each row height in px width="100%" > {({ index, style }) => ( // style contains absolute positioning injected by react-window <div style={style}>{items[index].name}</div> )} </List> )); ``` With 10,000 items at 70px each, the total scroll height is 700,000px. React-window renders only the ~9 visible rows at any point. Chrome Profiler shows scroll at 60fps where an unvirtualized list would drop to 15fps or below. ### State colocation to prevent tree-wide re-renders Moving hover state down to the component that actually needs it. ```tsx // ❌ Hover state in App: all children re-render on every mouse move function App() { const [hoveredId, setHoveredId] = useState<string | null>(null); return <ProductList items={products} hoveredId={hoveredId} onHover={setHoveredId} />; } // ✅ Hover state in the item itself: only that item re-renders function ProductCard({ product }: { product: Product }) { const [isHovered, setIsHovered] = useState(false); return ( <div onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} className={isHovered ? "highlighted" : ""} > {product.name} </div> ); } ``` This is often the biggest win in practice. Before reaching for `memo`, check whether the state that triggers re-renders actually needs to live that high in the tree. I have seen 40-component trees drop to single-digit re-renders just by moving one `useState` call down two levels.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.