patternjavascriptMajor
Virtual scrolling for rendering large lists without DOM bloat
Viewed 0 times
virtual scrollingwindowingtanstack-virtualreact-windowlarge listsDOM performance
Problem
Rendering 10,000 list items to the DOM at once creates ~10,000 DOM nodes. The browser must lay out, paint, and composite all of them. Scrolling becomes janky and initial render freezes the UI for seconds.
Solution
Render only the visible items plus a small overscan buffer. Update which items are rendered as the user scrolls.
// Using @tanstack/virtual (framework-agnostic)
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48, // estimated row height in px
overscan: 5,
});
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map(vItem => (
<div
key={vItem.key}
style={{ position: 'absolute', top: vItem.start, height: vItem.size }}
>
{items[vItem.index].name}
</div>
))}
</div>
</div>
);
}
// Using @tanstack/virtual (framework-agnostic)
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48, // estimated row height in px
overscan: 5,
});
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map(vItem => (
<div
key={vItem.key}
style={{ position: 'absolute', top: vItem.start, height: vItem.size }}
>
{items[vItem.index].name}
</div>
))}
</div>
</div>
);
}
Why
Virtual scrolling maintains a constant DOM node count (~20-30 items) regardless of data size. Scroll events trigger position updates but no new network requests. Memory and layout cost stay flat.
Gotchas
- Variable row heights require dynamic size measurement — use virtualizer's measureElement callback
- Accessibility: screen readers may not read off-screen items — test with ARIA and ensure keyboard navigation works
- Anchor scrolling (scrollIntoView) must account for the virtual offset, not the actual DOM position
- Search/filter on virtualized lists requires the full dataset in memory — not a pagination replacement
Code Snippets
Dynamic height measurement for variable-size rows
// Measure actual rendered item heights dynamically
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
measureElement: (element) => element.getBoundingClientRect().height,
});Context
When rendering lists with more than ~500 items, or any list that causes noticeable scroll jank
Revisions (0)
No revisions yet.