Skip to content

Handling Large Lists with Virtualization in React

Learn how to efficiently render thousands of list items using virtualization techniques, improving performance and user experience.

By 6 min read
  • React
  • Performance
  • Virtualization
  • TypeScript
  • Lists
Handling Large Lists with Virtualization in React

Rendering large lists in React can quickly become a performance bottleneck. When you have hundreds or thousands of items, rendering them all at once causes significant lag, memory issues, and poor user experience. Virtualization solves this by rendering only the visible items plus a small buffer.

The Problem with Large Lists

Consider a list with 10,000 items. Rendering all of them means:

  • Creating 10,000 DOM nodes
  • High memory consumption
  • Slow initial render
  • Laggy scrolling
  • Poor mobile performance
TSX
interface Item {
  id: string;
}

interface BadListProps {
  items: Item[];
}

// This will be slow with large datasets
function BadList({ items }: BadListProps) {
  return (
    <div>
      {items.map(item => (
        <ListItem key={item.id} item={item} />
      ))}
    </div>
  );
}

What is Virtualization?

Virtualization (also called windowing) renders only the items visible in the viewport plus a small buffer. As you scroll, items are dynamically added and removed from the DOM.

Using react-window

react-window is a lightweight, efficient virtualization library. Here's how to use it:

Basic Fixed Size List

TSX
import { FixedSizeList } from 'react-window';

interface Item {
  id: string;
}

interface VirtualizedListProps {
  items: Item[];
}

function VirtualizedList({ items }: VirtualizedListProps) {
  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
    <div style={style}>
      <ListItem item={items[index]} />
    </div>
  );

  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

Variable Size List

When items have different heights, use VariableSizeList:

TSX
import { VariableSizeList } from 'react-window';

interface Item {
  id: string;
  isExpanded: boolean;
}

interface VariableHeightListProps {
  items: Item[];
}

function VariableHeightList({ items }: VariableHeightListProps) {
  const getItemSize = (index: number): number => {
    // Return height based on item content
    return items[index].isExpanded ? 100 : 50;
  };

  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
    <div style={style}>
      <ListItem item={items[index]} />
    </div>
  );

  return (
    <VariableSizeList
      height={600}
      itemCount={items.length}
      itemSize={getItemSize}
      width="100%"
    >
      {Row}
    </VariableSizeList>
  );
}

Using react-virtual

react-virtual provides more flexibility and hooks-based API:

TSX
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';

interface Item {
  id: string;
}

interface VirtualizedListProps {
  items: Item[];
}

function VirtualizedList({ items }: VirtualizedListProps) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
    overscan: 5, // Render 5 extra items outside viewport
  });

  return (
    <div
      ref={parentRef}
      style={{ height: '600px', overflow: 'auto' }}
    >
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          width: '100%',
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualItem.size}px`,
              transform: `translateY(${virtualItem.start}px)`,
            }}
          >
            <ListItem item={items[virtualItem.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

Grid Virtualization

For two-dimensional lists (grids), use FixedSizeGrid:

TSX
import { FixedSizeGrid } from 'react-window';

interface Item {
  id: string;
}

interface VirtualizedGridProps {
  items: Item[];
}

function VirtualizedGrid({ items }: VirtualizedGridProps) {
  const Cell = ({ 
    columnIndex, 
    rowIndex, 
    style 
  }: { 
    columnIndex: number; 
    rowIndex: number; 
    style: React.CSSProperties;
  }) => (
    <div style={style}>
      <GridItem item={items[rowIndex * 5 + columnIndex]} />
    </div>
  );

  return (
    <FixedSizeGrid
      columnCount={5}
      columnWidth={200}
      height={600}
      rowCount={Math.ceil(items.length / 5)}
      rowHeight={150}
      width={1000}
    >
      {Cell}
    </FixedSizeGrid>
  );
}

Advanced Patterns

Dynamic Height Calculation

When item heights are unknown, measure them dynamically:

TSX
import { useState, useRef, useEffect } from 'react';
import { VariableSizeList } from 'react-window';

interface Item {
  id: string;
}

interface DynamicHeightListProps {
  items: Item[];
}

function DynamicHeightList({ items }: DynamicHeightListProps) {
  const [heights, setHeights] = useState<Record<number, number>>({});
  const listRef = useRef<VariableSizeList>(null);

  const getItemSize = (index: number): number => heights[index] || 50;

  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => {
    const rowRef = useRef<HTMLDivElement>(null);

    useEffect(() => {
      if (rowRef.current) {
        const height = rowRef.current.getBoundingClientRect().height;
        setHeights(prev => ({ ...prev, [index]: height }));
      }
    }, [index]);

    return (
      <div ref={rowRef} style={style}>
        <ListItem item={items[index]} />
      </div>
    );
  };

  return (
    <VariableSizeList
      ref={listRef}
      height={600}
      itemCount={items.length}
      itemSize={getItemSize}
      width="100%"
    >
      {Row}
    </VariableSizeList>
  );
}

Infinite Scrolling with Virtualization

Combine virtualization with infinite scrolling:

TSX
import { useState, useRef } from 'react';
import { FixedSizeList } from 'react-window';

interface Item {
  id: string;
}

function InfiniteVirtualizedList() {
  const [items, setItems] = useState<Item[]>([]);
  const [hasMore, setHasMore] = useState<boolean>(true);
  const listRef = useRef<FixedSizeList>(null);

  const loadMore = async (): Promise<void> => {
    const newItems = await fetchMoreItems();
    setItems(prev => [...prev, ...newItems]);
    setHasMore(newItems.length > 0);
  };

  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => {
    if (index === items.length - 1 && hasMore) {
      loadMore();
    }

    return (
      <div style={style}>
        <ListItem item={items[index]} />
      </div>
    );
  };

  return (
    <FixedSizeList
      ref={listRef}
      height={600}
      itemCount={hasMore ? items.length + 1 : items.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

Performance Considerations

Overscan

Overscan determines how many items to render outside the viewport. More overscan means smoother scrolling but more DOM nodes:

TSX
<FixedSizeList
  overscanCount={5} // Render 5 items above and below viewport
  // ... other props
/>

Memoization

Memoize your row component to prevent unnecessary re-renders:

TSX
interface RowProps {
  index: number;
  style: React.CSSProperties;
  data: Item[];
}

const Row = React.memo<RowProps>(({ index, style, data }) => (
  <div style={style}>
    <ListItem item={data[index]} />
  </div>
));

Key Extraction

Ensure stable keys for list items:

TSX
// Good: Use unique, stable IDs
items.map(item => <ListItem key={item.id} />)

// Bad: Using index as key
items.map((item, index) => <ListItem key={index} />)

When to Use Virtualization

Use virtualization when:

  • You have 100+ items
  • Items have complex rendering logic
  • Performance is critical
  • Mobile performance matters

Don't use virtualization when:

  • You have fewer than 50 items
  • Items are extremely simple
  • You need CSS scroll snap
  • You need native browser scrolling behavior

Real-World Performance

In production applications, virtualization has resulted in:

  • 95% reduction in initial render time for 10,000+ item lists
  • 90% reduction in memory usage
  • Smooth 60fps scrolling regardless of list size
  • Improved mobile performance significantly

Best Practices

  1. Measure item heights: Use estimateSize accurately
  2. Optimize overscan: Balance smoothness vs. performance
  3. Memoize row components: Prevent unnecessary re-renders
  4. Use stable keys: Ensure React can track items efficiently
  5. Handle dynamic content: Account for content that changes size
  6. Test on mobile: Virtualization is especially important on mobile devices

Virtualization is essential for building performant React applications that handle large datasets. Choose the right library for your needs and implement it correctly to see dramatic performance improvements.

Keep reading