šŸ“œ
Intermediate
11 min read

Virtual Scrolling & Windowing

Rendering large lists efficiently with virtual scrolling

Understanding Virtual Scrolling

Virtual scrolling (windowing) renders only the visible portion of a large list, dramatically improving performance when dealing with thousands or millions of items.

The Problem with Large Lists

// āŒ Bad: Rendering 10,000 items at once
function LargeList({ items }) {
  return (
    
{items.map(item => (
{item.name}
))}
); } // Problems: // - Creates 10,000 DOM nodes // - Long initial render time // - Heavy memory usage // - Slow scrolling performance

Basic Virtual Scrolling Implementation

function VirtualList({ items, itemHeight = 50, containerHeight = 600 }) {
  const [scrollTop, setScrollTop] = useState(0);
  
  // Calculate visible range
  const startIndex = Math.floor(scrollTop / itemHeight);
  const endIndex = Math.ceil((scrollTop + containerHeight) / itemHeight);
  
  // Get only visible items
  const visibleItems = items.slice(startIndex, endIndex + 1);
  
  // Calculate total height and offset
  const totalHeight = items.length * itemHeight;
  const offsetY = startIndex * itemHeight;
  
  const handleScroll = (e) => {
    setScrollTop(e.target.scrollTop);
  };
  
  return (
    
{visibleItems.map((item, index) => (
{item.name}
))}
); }

Using react-window

// Install: npm install react-window

import { FixedSizeList } from 'react-window';

// Simple fixed-height list
function MyList({ items }) {
  const Row = ({ index, style }) => (
    
{items[index].name}
); return ( {Row} ); } // Variable height list import { VariableSizeList } from 'react-window'; function VariableList({ items }) { const getItemSize = (index) => { // Return dynamic height based on content return items[index].content.length > 100 ? 100 : 50; }; const Row = ({ index, style }) => (

{items[index].title}

{items[index].content}

); return ( {Row} ); } // Grid layout import { FixedSizeGrid } from 'react-window'; function MyGrid({ items }) { const Cell = ({ columnIndex, rowIndex, style }) => { const index = rowIndex * 5 + columnIndex; return (
{items[index]?.name}
); }; return ( {Cell} ); }

Advanced: Dynamic Height with Auto-Sizing

import { VariableSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';

function DynamicList({ items }) {
  const listRef = useRef();
  const rowHeights = useRef({});
  
  // Measure row height after render
  const setRowHeight = (index, size) => {
    listRef.current.resetAfterIndex(0);
    rowHeights.current = { ...rowHeights.current, [index]: size };
  };
  
  const getRowHeight = (index) => {
    return rowHeights.current[index] || 80; // Default height
  };
  
  const Row = ({ index, style }) => {
    const rowRef = useRef();
    
    useEffect(() => {
      if (rowRef.current) {
        setRowHeight(index, rowRef.current.clientHeight);
      }
    }, [index]);
    
    return (
      

{items[index].title}

{items[index].content}

); }; return ( {({ height, width }) => ( {Row} )} ); }

Infinite Scrolling with Virtual List

import { FixedSizeList } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';

function InfiniteList() {
  const [items, setItems] = useState([]);
  const [hasMore, setHasMore] = useState(true);
  
  // Check if item is loaded
  const isItemLoaded = (index) => !hasMore || index < items.length;
  
  // Load more items
  const loadMoreItems = async (startIndex, stopIndex) => {
    const newItems = await fetchItems(startIndex, stopIndex);
    setItems(prev => [...prev, ...newItems]);
    
    if (newItems.length === 0) {
      setHasMore(false);
    }
  };
  
  const Row = ({ index, style }) => {
    const item = items[index];
    
    if (!isItemLoaded(index)) {
      return 
Loading...
; } return (
{item.name}
); }; return ( {({ onItemsRendered, ref }) => ( {Row} )} ); }

Virtual Scrolling with Intersection Observer

// Lightweight custom implementation
function useVirtualScroll(items, options = {}) {
  const {
    itemHeight = 50,
    overscan = 3, // Extra items to render above/below
    containerHeight = 600
  } = options;
  
  const [scrollTop, setScrollTop] = useState(0);
  const [isScrolling, setIsScrolling] = useState(false);
  const scrollTimeoutRef = useRef();
  
  const handleScroll = (e) => {
    setScrollTop(e.target.scrollTop);
    setIsScrolling(true);
    
    clearTimeout(scrollTimeoutRef.current);
    scrollTimeoutRef.current = setTimeout(() => {
      setIsScrolling(false);
    }, 150);
  };
  
  // Calculate visible range with overscan
  const startIndex = Math.max(
    0, 
    Math.floor(scrollTop / itemHeight) - overscan
  );
  const endIndex = Math.min(
    items.length - 1,
    Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
  );
  
  const visibleItems = items.slice(startIndex, endIndex + 1);
  const offsetY = startIndex * itemHeight;
  const totalHeight = items.length * itemHeight;
  
  return {
    visibleItems,
    offsetY,
    totalHeight,
    startIndex,
    handleScroll,
    isScrolling
  };
}

// Usage
function VirtualList({ items }) {
  const {
    visibleItems,
    offsetY,
    totalHeight,
    startIndex,
    handleScroll,
    isScrolling
  } = useVirtualScroll(items, {
    itemHeight: 50,
    containerHeight: 600,
    overscan: 5
  });
  
  return (
    
{visibleItems.map((item, index) => (
{isScrolling ? (
Loading...
) : ( )}
))}
); }

Performance Monitoring

// Measure virtual list performance
function usePerformanceMonitor() {
  const renderCount = useRef(0);
  const startTime = useRef(performance.now());
  
  useEffect(() => {
    renderCount.current++;
    
    const elapsed = performance.now() - startTime.current;
    
    if (elapsed > 1000) {
      console.log(`FPS: ${(renderCount.current / elapsed * 1000).toFixed(2)}`);
      renderCount.current = 0;
      startTime.current = performance.now();
    }
  });
}

// Compare performance
function ComparisonTest() {
  const items = Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `Item ${i}`
  }));
  
  // Regular list (slow)
  const RegularList = () => (
    
{items.map(item => (
{item.name}
))}
); // Virtual list (fast) const VirtualList = () => ( {({ index, style }) => (
{items[index].name}
)}
); }

Best Practices

  • Use virtual scrolling for lists with 100+ items
  • Add overscan (extra rendered items) for smoother scrolling
  • Use react-window for simple cases, react-virtualized for complex ones
  • Implement dynamic height calculation for variable content
  • Combine with infinite scrolling for large datasets
  • Use will-change: transform CSS for smoother scrolling
  • Avoid heavy computations in row components
  • Memoize row components with React.memo
  • Test performance with realistic data volumes
  • Consider grid virtualization for table-like layouts