React
React Performance Playbook: Memoization, Virtualization, and Renders
Concrete steps to reduce unnecessary renders and keep React UIs fast as data and components grow.

React performance issues usually show up as sluggish lists, heavy forms, or janky interactions. The main levers are fewer renders, cheaper renders, and rendering less DOM. Here’s a playbook that works in production.
Measure before you optimize
Use React DevTools Profiler to record a typical interaction (e.g. typing in a filter, opening a list). See which components re-render and how long they take. Fix the slowest and most frequent first. Don’t memoize everything by default; it adds cognitive load and can hide real issues.
Memoization: when it helps
React.memo skips re-renders when props are referentially equal. It helps when a parent re-renders often but the child’s props are usually unchanged. Combine with stable callbacks (useCallback) and stable objects/arrays (useMemo) so that “unchanged” is actually true.
const ExpensiveRow = memo(function ExpensiveRow({ item, onSelect }) {
// Heavy render
return <tr onClick={() => onSelect(item.id)}>...</tr>;
});
function Table({ items, selectedId, onSelect }) {
const handleSelect = useCallback((id) => {
onSelect(id);
}, [onSelect]);
return (
<table>
{items.map((item) => (
<ExpensiveRow
key={item.id}
item={item}
onSelect={handleSelect}
/>
))}
</table>
);
}If the parent creates a new onSelect or new item reference every time, memo is useless. Stabilize those first. Use memo for list items, heavy cells, or components that sit under frequently updating state.
Virtualization: long lists
For long lists (hundreds or thousands of rows), render only the visible window. Libraries like react-window or TanStack Virtual mount a small number of DOM nodes and recycle them as the user scrolls. That keeps layout and paint cost constant regardless of list length.
import { FixedSizeList } from "react-window";
function VirtualList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
<ExpensiveRow item={items[index]} />
</div>
);
return (
<FixedSizeList
height={400}
itemCount={items.length}
itemSize={48}
width="100%"
>
{Row}
</FixedSizeList>
);
}Virtualize any list that can grow large; it’s often the biggest win. Keep row components as light as possible so scrolling stays smooth.
Splitting and lazy loading
Code-split routes and heavy components with React.lazy and Suspense. That reduces initial JS and speeds up first load. Lazy load below-the-fold or modal content so the critical path stays small. Use dynamic imports for big charts, editors, or admin-only sections.
State shape and subscription
Lift state only as high as needed. If only a subtree needs an update, keep state there so the rest of the tree doesn’t re-render. With Zustand or Redux, use selectors that return primitives or stable references so components only re-render when their slice actually changes. Avoid storing derived data in state; compute it in render or with useMemo so you don’t invalidate unnecessarily.
Summary
Profile first, then reduce renders (memo + stable props), virtualize long lists, and split/lazy load heavy code. Apply memoization where you’ve measured re-renders; use virtualization for any list that can get long. This playbook keeps React UIs responsive as they grow.