Virtualizing Massive Lists with TanStack Virtual0%

Virtualizing Massive Lists with TanStack Virtual

Beginner12 min read•Updated: Jul 11, 2026
Study MaterialsCheatsheet

Virtualizing Massive Lists with TanStack Virtual

Rendering lists containing 10,000 to 100,000 DOM elements causes massive memory overhead, DOM layout thrashing, and high input latency. Virtualization solves this by rendering only the visible items currently inside the scrollable viewport (plus a small overscan buffer), recycling DOM elements dynamically as the user scrolls. @tanstack/react-virtual is the enterprise industry standard for performant headless virtualization in React.


1. How DOM Virtualization Works

Without virtualization, rendering 50,000 items creates 50,000 physical DOM nodes in the browser tree:

  • High DOM memory consumption (>300 MB).
  • Severe frame rate drops during scrolling.
  • Slow Garbage Collection pauses.

With windowing/virtualization:

  • If the viewport height fits 15 items, only ~25 DOM nodes exist at any moment.
  • As the user scrolls, item nodes at the top are unmounted or recycled for items appearing at the bottom.
  • A virtual scroll container maintains total proportional scroll height using CSS transforms or absolute positioning.
Output
Total List (50,000 items) - Virtual Height: 2,500,000 px
┌──────────────────────────────────────┐
│ (Unrendered Items 1 - 1,200) │
├──────────────────────────────────────┤ ◄── Top of Scrollport
│ Visible Rendered Item 1,201 │
│ Visible Rendered Item 1,202 │
│ Visible Rendered Item 1,203 │
│ Visible Rendered Item 1,204 │
│ Visible Rendered Item 1,205 │
├──────────────────────────────────────┤ ◄── Bottom of Scrollport
│ (Unrendered Items 1,206 - 50,000) │
└──────────────────────────────────────┘

2. Implementing useVirtualizer with @tanstack/react-virtual

Bash / Terminal
npm install @tanstack/react-virtual
TSX
import React, { useRef } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
 
interface LogEntry {
id: number;
timestamp: string;
level: "INFO" | "WARN" | "ERROR";
message: string;
}
 
// Generate 50,000 mock audit logs
const AUDIT_LOGS: LogEntry[] = Array.from({ length: 50000 }, (_, index) => ({
id: index + 1,
timestamp: new Date(Date.now() - index * 60000).toISOString(),
level: index % 10 === 0 ? "ERROR" : index % 3 === 0 ? "WARN" : "INFO",
message: `Transaction trace payload verification status for microservice chunk #${index + 1}`,
}));
 
export function VirtualizedLogViewer() {
const parentRef = useRef<HTMLDivElement>(null);
 
const virtualizer = useVirtualizer({
count: AUDIT_LOGS.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48, // Estimated row height in px
overscan: 5, // Pre-render 5 rows above and below viewport
});
 
return (
<div className="p-8 bg-slate-900 min-h-screen text-slate-100">
<h1 className="text-xl font-bold mb-4">Enterprise Virtualized Log Viewer (50,000 Nodes)</h1>
 
{/* Outer scrollable viewport */}
<div
ref={parentRef}
className="h-[500px] w-full max-w-4xl overflow-y-auto border border-slate-800 rounded-lg bg-slate-950"
>
{/* Inner container setting the full scrollable virtual height */}
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: "100%",
position: "relative",
}}
>
{/* Only rendered visible virtual items */}
{virtualizer.getVirtualItems().map((virtualRow) => {
const log = AUDIT_LOGS[virtualRow.index];
 
return (
<div
key={virtualRow.key}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
className="flex items-center justify-between px-4 border-b border-slate-900 hover:bg-slate-900/60 font-mono text-xs"
>
<span className="text-slate-500 w-16">#{log.id}</span>
<span className="text-slate-400 w-44">{log.timestamp}</span>
<span
className={`w-16 font-bold ${
log.level === "ERROR"
? "text-rose-400"
: log.level === "WARN"
? "text-amber-400"
: "text-emerald-400"
}`}
>
[{log.level}]
</span>
<span className="truncate flex-1 text-slate-300">{log.message}</span>
</div>
);
})}
</div>
</div>
</div>
);
}

3. Dynamic Height Measurement

When list items have unpredictable variable heights (e.g. comments with variable text lengths or user attachments), use measureElement:

TSX
<div
key={virtualRow.key}
data-index={virtualRow.index}
ref={virtualizer.measureElement} // Measures DOM height dynamically
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
transform: `translateY(${virtualRow.start}px)`,
}}
>
{/* Variable height content */}
</div>

4. Key Virtualization Trade-Offs

  • Ctrl+F Find in Page: Unrendered items are not present in DOM; in-page browser search won't locate them unless you provide an in-app custom search bar.
  • Accessibility: Ensure ARIA attributes like aria-rowcount and aria-rowindex are passed so screen readers understand total list bounds.

Interactive Quiz

Practice Quiz

Question 1
What is the core mechanism behind DOM list virtualization?
Question 2
What is the purpose of the overscan option in TanStack Virtual?
Question 3
Why is translateY used to position virtual list items instead of setting standard top CSS properties?
Question 4
How does TanStack Virtual handle rows with dynamic, unknown text heights?
Question 5
What is a known limitation of DOM virtualization that engineers must account for?
Next Lesson

Web Workers in React for CPU-Intensive Background Tasks

Continue learning with hands-on practice, examples, and exercises in the upcoming topic.

Practice Quiz

Test your understanding of this lesson with 5 questions. Each question has one correct answer.

PrevNext