Virtual scrolling renders only the visible items in a list, dramatically improving performance for lists with thousands of items. Instead of rendering 10,000 DOM nodes, you render only the ~20 that are visible on screen.
Using @tanstack/react-virtual (recommended):
1import { useVirtualizer } from "@tanstack/react-virtual";2import { useRef } from "react";34function VirtualList({ items }: { items: string[] }) {5 const parentRef = useRef<HTMLDivElement>(null);67 const virtualizer = useVirtualizer({8 count: items.length,9 getScrollElement: () => parentRef.current,10 estimateSize: () => 50, // estimated row height in pixels11 overscan: 5, // render 5 extra items above/below viewport12 });1314 return (15 <div16 ref={parentRef}17 style={{ height: "500px", overflow: "auto" }}18 >19 <div20 style={{21 height: `${virtualizer.getTotalSize()}px`,22 width: "100%",23 position: "relative",24 }}25 >26 {virtualizer.getVirtualItems().map((virtualRow) => (27 <div28 key={virtualRow.index}29 style={{30 position: "absolute",31 top: 0,32 left: 0,33 width: "100%",34 height: `${virtualRow.size}px`,35 transform: `translateY(${virtualRow.start}px)`,36 }}37 >38 <div style={{ padding: "8px 16px", borderBottom: "1px solid #eee" }}>39 {items[virtualRow.index]}40 </div>41 </div>42 ))}43 </div>44 </div>45 );46}
Manual implementation — for learning:
1function ManualVirtualList({2 items,3 itemHeight = 50,4 containerHeight = 500,5}: {6 items: string[];7 itemHeight?: number;8 containerHeight?: number;9}) {10 const [scrollTop, setScrollTop] = useState(0);1112 const startIndex = Math.floor(scrollTop / itemHeight);13 const endIndex = Math.min(14 items.length - 1,15 Math.floor((scrollTop + containerHeight) / itemHeight)16 );17 const visibleItems = items.slice(startIndex, endIndex + 1);1819 return (20 <div21 style={{ height: containerHeight, overflow: "auto" }}22 onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}23 >24 <div style={{ height: items.length * itemHeight, position: "relative" }}>25 {visibleItems.map((item, index) => (26 <div27 key={startIndex + index}28 style={{29 position: "absolute",30 top: (startIndex + index) * itemHeight,31 height: itemHeight,32 width: "100%",33 }}34 >35 {item}36 </div>37 ))}38 </div>39 </div>40 );41}
When to use virtual scrolling:
When NOT to use: