100,000 Rows Without Lag
Anyone who writes 10,000 React elements into the DOM is building an application that is not usable for people. Virtualization renders only the visible rows, everything else is math. How to implement this with TanStack Virtual, react-window and custom implementations.
Table of contents
- 1. The problem: DOM nodes cost memory and time
- 2. The basic principle of virtualization
- 3. TanStack Virtual: the modern standard
- 4. Implementing fixed-height lists
- 5. Dynamic heights: variable row heights
- 6. Horizontal scrolling and 2D grids
- 7. Infinite scroll with virtualization
- 8. Common pitfalls and how to solve them
- 9. Libraries compared
- 10. Summary
- 11. FAQ
1. The problem: DOM nodes cost memory and time
A single React list entry creates several nodes in the DOM, each with its own layout context, event listener infrastructure and rendering budget. With 1,000 entries this is still manageable in modern browsers. With 10,000 entries, the initial render starts taking several seconds, even if all of these entries are below the viewport and not visible to the user. With 100,000 entries, the application is simply unusable on average hardware: scrolling stutters, interactions are delayed by hundreds of milliseconds, and the tab consumes gigabytes of RAM.
The problem is not React specific, it is a fundamental browser problem. The browser has to hold all DOM nodes in its layout tree, calculate reflows for entire lists and coordinate paint operations for the whole visible area. Virtualization solves this problem radically: instead of writing all elements into the DOM, only the elements that are currently in the viewport or just outside it are rendered. The key lies in knowing the position of every element mathematically without rendering it, and dynamically recalculating the visible elements on scroll events.
2. The basic principle of virtualization
A virtual list works with three building blocks: an outer container with a fixed height and overflow: auto, an inner container that simulates the total height of all items (so the scrollbar is correct), and the actually rendered items, which are positioned at the correct scroll position with position: absolute and a calculated transform: translateY().
The scroll event updates the visible range: React calculates which items are in the current viewport range and renders only those. All other items are not in the DOM, they exist only as math. This means the DOM depth stays constant no matter whether the list has 100 or 1,000,000 entries. The rendering effort scales with the viewport size, not with the dataset size. That is the fundamental performance win of React virtualization.
// TanStack Virtual v3: virtual list with fixed row height
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
interface Row {
id: number;
name: string;
email: string;
}
function VirtualProductList({ rows }: { rows: Row[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: rows.length, // total number of items (e.g. 100_000)
getScrollElement: () => parentRef.current,
estimateSize: () => 56, // estimated row height in px
overscan: 10, // render 10 extra rows above/below viewport
});
return (
// Outer container: fixed height, scrollable
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
{/* Inner container: simulates total list height for correct scrollbar */}
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.key}
data-index={virtualRow.index}
ref={virtualizer.measureElement} // enables dynamic height measurement
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start}px)`,
}}
>
<ProductRow row={rows[virtualRow.index]} />
</div>
))}
</div>
</div>
);
}
3. TanStack Virtual: the modern standard
TanStack Virtual v3 is the de-facto standard for React virtualization in 2026. The library is framework agnostic (React, Vue, Solid, Svelte), has a clean API and supports both fixed-height and dynamic-height lists natively. Unlike older libraries such as react-window, TanStack Virtual does without rigid component APIs, it is a headless virtualizer that only supplies the calculations and leaves full control over rendering to the developer. That makes integration with Tailwind CSS, complex row layouts and custom scrollbar implementations considerably easier.
The most important parameters of the useVirtualizer hook: count (total number of items), getScrollElement (reference to the scrollable container), estimateSize (initial estimate of item height), and overscan (number of extra items rendered above and below the viewport). The overscan attribute is crucial for smooth scrolling: too little overscan leads to visible flicker on fast scrolling, too much overscan increases the rendering effort unnecessarily. 5 to 15 items is typically the right range.
4. Implementing fixed-height lists
The simplest and most performant form of React virtualization is the fixed-height list: all items have the same height, which makes position calculation trivial. The virtualizer does not need to measure, it calculates all positions from the known height and the index. That is O(1) per item instead of O(n) for dynamic heights. For tables, file lists, search results and product lists with uniform layout, fixed height is the first choice.
A critical point with fixed-height lists: the estimateSize function must match the actually rendered height exactly, including padding, border and margin of the row container. If the estimate is off, gaps or overlapping rows appear while scrolling. Most reliable approach: measure the actual row height once with the browser DevTools and pass it in as a constant value. In responsive designs that have different heights at different viewport widths, the value must be calculated dynamically accordingly.
5. Dynamic heights: variable row heights
Dynamic-height virtualization is considerably more complex than fixed height, but necessary for many real world use cases: comment lists with variable text content, feeds with images, chat messages or tables with multi-line content. TanStack Virtual solves the problem elegantly with the measureElement callback: every rendered item measures its actual height and reports this value back to the virtualizer. The virtualizer then updates the position calculation for all subsequent items.
The measuring happens with a ResizeObserver internally in the virtualizer and is non-blocking. On the first render, items are positioned with the estimated height, after measuring, the virtualizer corrects the positions. That leads to a brief layout shift on first render, which is barely noticeable to the user if the estimate is close to reality. A good strategy: a generous estimate for the median of the expected heights, combined with a small overscan, to minimize layout shifts during fast scrolling.
// Dynamic height virtualization with measureElement
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef, useCallback } from 'react';
function DynamicHeightFeed({ posts }: { posts: Post[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: posts.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 200, // generous estimate for median post height
overscan: 5,
});
// measureElement enables dynamic height measurement via ResizeObserver
const measureRef = useCallback(
(node: HTMLDivElement | null) => {
if (node) virtualizer.measureElement(node);
},
[virtualizer]
);
return (
<div ref={parentRef} style={{ height: '100vh', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.key}
data-index={virtualRow.index} // required for measureElement to work
ref={measureRef}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start}px)`,
}}
>
<PostCard post={posts[virtualRow.index]} />
</div>
))}
</div>
</div>
);
}
6. Horizontal scrolling and 2D grids
Horizontal React virtualization follows the same principle as the vertical variant, just on the X axis. TanStack Virtual supports horizontal lists with the horizontal: true parameter. For 2D grids, such as a table with thousands of rows and columns, two virtualizers are combined: one for the rows and one for the columns. Only the cells that lie at the intersection of the visible row and column range are rendered. That is the key to displaying spreadsheet-like amounts of data, which neither Excel nor Google Sheets solve any differently internally.
A 2D virtualizer for a 10,000x100 grid typically renders only 15 to 20 rows and 8 to 12 columns simultaneously at a standard viewport, so at most 240 cells instead of 1,000,000. The initial render happens in milliseconds, scrolling is buttery smooth. The implementation requires careful handling of the scroll container: the outer container must be scrollable both vertically and horizontally, and the cells must sit in an absolutely positioned 2D grid, not in an HTML table element (which would make the position calculation considerably harder).
7. Infinite scroll with virtualization
The combination of infinite scroll and virtualization is the most powerful pattern for large, paginated datasets. The user scrolls through a list that seemingly never ends, in reality new data is loaded and inserted into the virtualized dataset once the end is reached. The critical detail: on loading more data, the virtualizer must be informed of the new total count without changing the scroll position. TanStack Virtual handles this automatically when count is updated.
The trigger logic for loading more data is another important pattern: a sentinel element is inserted at the end of the virtual items and observed with an IntersectionObserver. When the sentinel becomes visible, the next batch of data is requested. This is more efficient than scroll-event based triggers, because IntersectionObserver does not run on the main thread and does not require expensive getBoundingClientRect calls. Combined with React Query and the fetchNextPage function of useInfiniteQuery, this results in a complete, robust infinite scroll pattern.
8. Common pitfalls and how to solve them
The most common problem when implementing a virtual list: the scroll container configuration. The outer container must have a fixed height and overflow: auto or overflow: scroll. If the height comes from a flex container or grid layout, it must be ensured that the container actually receives a calculated pixel height, not height: auto. This is the most common cause of a virtualizer not scrolling or rendering all items at once.
A second common problem: components inside the virtualized list that lose their state on remount. Since the virtualizer removes items from the DOM and reinserts them, all components are unmounted and remounted whenever they scroll into the viewport. State inside these components is lost. The solution: state that needs to persist beyond scroll events must be held outside the virtualized item, in a store, a parent state or a caching layer such as React Query.
9. Libraries compared
The choice of virtualization library depends on the specific requirements: simplicity vs. flexibility, fixed vs. dynamic heights, community support and bundle size.
| Library | Dynamic heights | Headless | Bundle | Recommendation |
|---|---|---|---|---|
| TanStack Virtual v3 | Yes (native) | Yes | ~5 kB | New projects 2026 |
| react-window | Only with plugin | No | ~6 kB | Existing projects |
| react-virtuoso | Yes | No | ~25 kB | Simple integration |
| Custom implementation | Possible | Yes | 0 kB | Only for special requirements |
10. Summary
React virtualization is the only realistic solution for lists with more than a few thousand entries. The basic principle is stable and library independent: render only visible items, simulate total height mathematically, recalculate positions on scroll events. TanStack Virtual v3 implements this principle as a headless virtualizer with full control over rendering, ideal for modern React projects with complex layouts.
The key points: fixed-height lists are always simpler and more performant than dynamic-height lists, use uniform row heights whenever possible. The scroll container must have an explicit pixel height. State in virtualized items is lost on unmount, use external state for persistent data. Infinite scroll with an IntersectionObserver trigger and React Query is the most robust pattern for paginated datasets. And: overscan between 5 and 15 items is the right compromise between smooth scrolling and rendering overhead.
React Virtual List, the key points at a glance
Render only visible items
The virtualizer renders only items in the viewport plus overscan. DOM depth stays constant, no matter whether there are 100 or 100,000 items.
Scroll container setup
Outer container needs a fixed pixel height and overflow: auto. Inner container simulates total height. Items positioned with position: absolute and translateY.
TanStack Virtual v3
Headless, 5 kB, supports fixed and dynamic heights natively. measureElement for automatic height measurement via ResizeObserver.
State management
Items are unmounted on scroll. State in virtualized components is lost, keep it in an external store or React Query.
Mironsoft
React performance optimization, virtualization and large datasets
Need to optimize a React application with large lists?
We analyze performance problems in React applications, implement virtualization with TanStack Virtual and optimize infinite scroll patterns for large datasets.
Performance audit
Identify React Profiler analysis, rendering bottlenecks and DOM size problems
Implementation
Implement virtual lists, dynamic heights and 2D grids with TanStack Virtual
Infinite scroll
IntersectionObserver trigger and React Query integration for robust paginated loading