To implement a high-performance virtualized list with fixed row height, render only the visible slice plus a small buffer, manage scroll position to compute start index, and use a spacer div to preserve full scroll height.
-
Approach: compute totalHeight = itemCount * rowHeight; on scroll compute startIndex = floor(scrollTop / rowHeight) - overscan, clamp to [0, itemCount - visibleCount]; render items from startIndex to endIndex and absolutely position them inside a container.
-
Component API:
- <VirtualList
itemCount={number}
rowHeight={number}
height={number} // viewport height
renderItem={(index) => ReactNode}
overscan={number} // optional
/>
- Implementation:
javascript
import React, { useRef, useState, useEffect, useCallback } from 'react';
export default function VirtualList({ itemCount, rowHeight, height, renderItem, overscan = 3 }) {
const containerRef = useRef(null);
const [scrollTop, setScrollTop] = useState(0);
const onScroll = useCallback((e) => {
setScrollTop(e.currentTarget.scrollTop);
}, []);
const totalHeight = itemCount * rowHeight;
const viewportCount = Math.ceil(height / rowHeight);
const startIndex = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
const endIndex = Math.min(itemCount - 1, startIndex + viewportCount + overscan * 2);
const items = [];
for (let i = startIndex; i <= endIndex; i++) {
const style = {
position: 'absolute',
top: i * rowHeight,
height: rowHeight,
width: '100%',
};
items.push(
<div key={i} style={style}>
{renderItem(i)}
</div>
);
}
return (
<div
ref={containerRef}
onScroll={onScroll}
style={{ position: 'relative', overflowY: 'auto', height }}
>
<div style={{ height: totalHeight, position: 'relative' }}>
{items}
</div>
</div>
);
}
- Key concepts:
- Windowing reduces DOM nodes from O(N) to O(viewportCount + overscan)
- Use absolute positioning to avoid layout reflows per item
- Keep renderItem pure or memoized to avoid unnecessary re-renders
- Complexity:
- Time: O(visibleCount) per render/scroll event
- Space: O(visibleCount) DOM nodes
- Testing & performance:
- Measure FPS and frame cost during fast scroll (DevTools Performance, Lighthouse)
- Use synthetic dataset of 1,000,000 items; verify memory usage and initial render quickness
- Test with different rowHeight, viewport sizes, and overscan values
- Use React Profiler to detect wasted renders; memoize renderItem or use React.memo
- Edge cases:
- rowHeight <= 0 or non-integer -> validate and throw
- itemCount = 0 -> render empty state
- Rapid scroll / wheel fling -> throttle or use requestAnimationFrame to batch setScrollTop updates
- Dynamic container height -> observe resize (ResizeObserver) and recompute viewportCount
- Variable row heights (not supported) -> document limitation or implement measurement + range map
This solution is simple, performant for fixed-height rows, and extensible for optimizations (windowing virtualization libraries, recycling DOM nodes, virtualization of horizontal lists).