List Virtualization
Render only visible rows for large lists: windowing mental model, libraries, keys, and accessibility tradeoffs.
- react
- list-virtualization
Mounting 50,000 DOM nodes freezes the main thread. List virtualization (windowing) renders only the rows near the viewport plus a small overscan buffer, recycling row components as the user scrolls.
Docs / libs: TanStack Virtual, react-window.
Mental model
- Know total count and row height (fixed or measured).
- From scroll offset, compute first/last visible index.
- Render only those items, positioned absolutely (or via transforms) inside a tall spacer that preserves scroll height.
// Conceptual — prefer a battle-tested library in production
function VirtualList({ items, rowHeight, height }) {
const [scrollTop, setScrollTop] = useState(0);
const start = Math.floor(scrollTop / rowHeight);
const visibleCount = Math.ceil(height / rowHeight) + 2;
const slice = items.slice(start, start + visibleCount);
return (
<div
style={{ height, overflow: 'auto' }}
onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
>
<div style={{ height: items.length * rowHeight, position: 'relative' }}>
{slice.map((item, i) => {
const index = start + i;
return (
<div
key={item.id}
style={{
position: 'absolute',
top: index * rowHeight,
height: rowHeight,
left: 0,
right: 0,
}}
>
{item.label}
</div>
);
})}
</div>
</div>
);
}
Keys and state
Use stable item ids, not indices — recycling makes index keys catastrophic (preserving state). Local state in a row may reset as the component is reused for another item unless you key by id carefully and design for reuse.
Variable height and grids
Variable heights need measurement (libraries handle this). Grids virtualize 2D windows. Nested scrollers need clear ownership of scroll containers.
Accessibility
Virtualization can break “find in page,” screen reader virtual buffers, and tabindex order. Provide:
- sensible
aria-rowcount/ set size where applicable - keyboard scroll and focus management
- an alternative for export/search when the full list is not in the DOM
When not to virtualize
Hundreds of simple rows often fine. Virtualize when profiling shows mount/layout cost. Also consider pagination or infinite query windows from the server.
Interview out-loud
“Virtualization renders only visible rows plus overscan inside a spacer that preserves scroll height. I use stable ids, a known or measured row height, and a library for edge cases. I call out a11y and find-in-page tradeoffs, and I virtualize only after measuring.”
Related on this site
Further reading
Edge cases worth rehearsing
Interviewers and production incidents cluster around the same edges: first render versus update, empty and loading states, Strict Mode double setup, concurrent interruptions, and what happens when identity (key, route params, user id) changes mid-edit. Walk one concrete user journey end-to-end — open, edit, navigate away, come back — and say which state survives.
Prefer fixing data flow and ownership before reaching for memoization or micro-optimizations. Prefer event handlers over effects when a user action is the trigger. Prefer deriving values during render over mirroring props into state. Prefer stable list keys from business ids. Measure with the profiler when performance is the claim.
When you cite an API, mention one failure mode: abort on unmount, serializable props across server/client boundaries, focus restoration for dialogs, or cache invalidation after a mutation. Specific beats generic every time.
Edge cases worth rehearsing
Interviewers and production incidents cluster around the same edges: first render versus update, empty and loading states, Strict Mode double setup, concurrent interruptions, and what happens when identity (key, route params, user id) changes mid-edit. Walk one concrete user journey end-to-end — open, edit, navigate away, come back — and say which state survives.
Prefer fixing data flow and ownership before reaching for memoization or micro-optimizations. Prefer event handlers over effects when a user action is the trigger. Prefer deriving values during render over mirroring props into state. Prefer stable list keys from business ids. Measure with the profiler when performance is the claim.
When you cite an API, mention one failure mode: abort on unmount, serializable props across server/client boundaries, focus restoration for dialogs, or cache invalidation after a mutation. Specific beats generic every time.
Related guides
- Accessibility Patterns in ReactPractical React a11y: labels, focus management, keyboard, live regions, and composition patterns that stay accessible.
- Avoid Prop Drilling with CompositionStop threading props through intermediates: children slots, inversion of control, and when context is the right escape hatch.
- Batching State UpdatesHow React 18+ batches setState in events, timeouts, and promises: when updates flush and why double setState still works.
- Children Prop PatternsUsing children and slot props for flexible APIs: wrappers, compound components, and when to prefer explicit props.
- Client Component BoundariesWhere to put use client: push interactivity to leaves, serializable props, and children as server slots.