Back to all notes

Virtualization Pattern: Render Thousands of Rows Without Lag

Use a virtualized list to render only what's visible and keep the DOM lean.

2 min read

Introduction

Virtualization is a rendering technique where only the items visible in the viewport (and a small buffer) are mounted in the DOM. It lets you handle thousands of rows smoothly without overloading the browser.

Why this matters

Rendering hundreds or thousands of DOM nodes tanks performance. Virtualization libraries render only the rows in the viewport and recycle them as you scroll.

The problem

A regular loop renders all 1,000 items at once—wasted work and slow UI.

Inefficient approach

Rendering every row eagerly bloats the DOM and tanks scroll performance:

const items = Array.from({ length: 1000 }, (_, index) => `Item ${index + 1}`);

function App() {
  return (
    <div>
      <h1>Virtualization Pattern</h1>
      <p>Rendering {items.length} items efficiently:</p>

      <div style={{ height: 400, overflow: "auto" }}>
        {items.map((item, index) => (
          <div key={index} style={{ padding: "8px" }}>{item}</div>
        ))}
      </div>
    </div>
  );
}

export default App;

Problems you’ll feel:

  • The browser lays out and paints 1,000 nodes up front.
  • Scrolling triggers costly reflows.
  • Memory usage spikes, especially on mobile.

The solution

Swap the naive list for a virtualized list. This example uses a List API provided in the lesson code to mirror react‑window behavior.

import { useState, useMemo } from "react";

const items = Array.from({ length: 1000 }, (_, index) => `Item ${index + 1}`);

// Simplified virtualization - only render visible items
function VirtualizedList({ items, containerHeight = 400, itemHeight = 35 }) {
  const [scrollTop, setScrollTop] = useState(0);

  const visibleRange = useMemo(() => {
    const start = Math.floor(scrollTop / itemHeight);
    const end = Math.min(
      start + Math.ceil(containerHeight / itemHeight) + 1,
      items.length
    );
    return { start, end };
  }, [scrollTop, containerHeight, itemHeight, items.length]);

  const visibleItems = items.slice(visibleRange.start, visibleRange.end);
  const offsetY = visibleRange.start * itemHeight;

  return (
    <div
      style={{
        height: containerHeight,
        overflow: "auto",
        position: "relative"
      }}
      onScroll={(e) => setScrollTop(e.target.scrollTop)}
    >
      <div style={{ height: items.length * itemHeight, position: "relative" }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item, idx) => (
            <div
              key={visibleRange.start + idx}
              style={{ height: itemHeight, padding: "8px" }}
            >
              {item}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Virtualization Pattern</h1>
      <p>Rendering {items.length} items efficiently:</p>
      <VirtualizedList items={items} />
    </div>
  );
}

export default App;

Step-by-step

  1. Generate a large items array for demonstration.
  2. Create a Row component that renders items[index] and applies style.
  3. Replace the naive loop with a virtualized <List> component.
  4. Set a fixed height and consistent rowHeight to enable virtualization.
  5. Verify smooth scrolling and minimal DOM nodes in DevTools.

Tips

  • With react‑window, prefer FixedSizeList with itemCount, itemSize, and itemData.
  • Keep row components pure and cheap—avoid expensive calculations inside the renderer.
  • Give the list a fixed height; virtualization needs a viewport to work.

Knowledge base

Problem snippet:

<div style={{ height: 400 }}>
  {items.map((item, index) => (
    <div key={index}>{item}</div>
  ))}
</div>

Solution snippet:

<List
  height={400}
  rowCount={items.length}
  rowHeight={35}
  rowComponent={Row}
  rowProps={{ items }}
/>