React Virtual List: 100,000 Rows Without Lag
AI generated
</>
{ }
React · Virtualization · Performance · TanStack Virtual
React Virtual List:
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.

15 min read TanStack Virtual · react-window · Windowing · Dynamic Heights React 18 · React 19 · TypeScript

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

11. FAQ: React Virtual List

1From what point does a virtual list make sense?
From around 500 to 1000 visible items in the DOM. With complex layouts, earlier. With simple text rows, only starting at several thousand items.
2TanStack Virtual vs. react-window?
TanStack Virtual is headless and flexible. react-window has ready-made list components but less flexibility. For new projects: TanStack Virtual.
3What is overscan?
Extra items rendered above/below the viewport. Too little means flicker while scrolling. 5 to 15 items is the right compromise.
4Handling dynamic heights?
measureElement in TanStack Virtual, every item measures its actual height via ResizeObserver. Generous estimate for estimateSize reduces layout shift.
5State lost when scrolling?
Yes, items are unmounted. Keep persistent state in React Query, an external store or a parent state.
6Infinite scroll with virtualization?
Sentinel element with IntersectionObserver. When visible: load next page, update count. React Query useInfiniteQuery for a complete pattern.
7Virtualizer does not scroll, why?
Most common problem: no explicit height pixel on the scroll container. height: auto is not enough. Container needs a calculated pixel height and overflow: auto.
8Combine virtualizer with HTML table?
Not directly. Use a div-based CSS Grid layout, visually identical, compatible with the virtualizer's absolute positioning.
9How does a 2D virtualizer work?
Two useVirtualizer instances: one for rows, one for columns. Only cells at the intersection are rendered. TanStack Virtual with the horizontal parameter for columns.
10Does virtualization affect SEO?
Yes with CSR. Search engines see only initial DOM items. SEO relevant lists: SSR or static generation for the first n entries, virtualization only for the scrollable view.