Row count is almost never the constraint that breaks a React grid. Update frequency, referential stability, and the browser's scroll-container pixel cap are.
Virtualize above ~1k rows and the row count stops mattering. Every serious grid renders about forty rows, whether the dataset is ten thousand or ten million [10]. The trace you are looking for — DOM cost climbing with dataset size — does not exist. It was flattened in 2015 and people are still buying grids to fix it.
| Grid | JS gzip | Ready (median) | Scroll settle | Mounted cells |
|---|---|---|---|---|
| React Data Grid ⭐ 7.7k | 72.3 KB | 367.6 ms | 21.2 ms | 264 |
| TanStack Table + Virtual ⭐ 28k | 78.7 KB | 369.3 ms | 21.7 ms | 444 |
| Ace Grid Core | 224.5 KB | 387.4 ms | 28.1 ms | 220 |
| AG Grid Community ⭐ 15k | 358.7 KB | 443.2 ms | 29.8 ms | 360 |
| Handsontable ⭐ 22k | 380.9 KB | 872.8 ms | 49.8 ms | 198 |
Read the flatness, not the ranking. Scroll settle is 21–30 ms across the entire field. The 76 ms spread between fastest and slowest (excluding Handsontable) is bundle parse and init — not scrolling. Nobody is slow at 50k rows. Below ~100k, pick on features. MUI X Community was excluded from the ranking outright: its Community tier cannot virtualize past 100 rows without pagination [6].
This is the channel that actually pegs, and it has nothing to do with how many rows you loaded. It is decided by whether a cell update enters the React reconciler at all.
Every cell is a React element. Users report the grid becoming "slow and nearly unusable" at ten row-updates per second on a 400-row window — where the plain HTML table it replaced handled a hundred [13]. The grid memoizes cells, but React.memo is defeated at the root the moment a caller passes an unstable non-primitive prop [14]. ⚠ MUI X is a display grid at scale, not a streaming grid.
Framework-agnostic core; React only at the edges. applyTransactionAsync() coalesces streaming ticks into a single flush every 50 ms — the reconciler never sees them [12]. This, not row count, is why AG Grid is the trading-blotter default. Handsontable shares the same shape.
The grid you adopted to handle "big data" is ten times slower at updating four hundred rows than the <table> you deleted [13]. Separately: an open issue reports DataGridPro leaking ~199 MB per refresh at 100k rows, reaching 4.7 GB, with retained CellWidget instances implicating renderCell [28].
Headless means zero built-in re-render cost and zero built-in protection. The classic TanStack Table ⭐ 28k blow-up is not a slow render path — it is a new array identity on every render [22].
// memoized the base array — still broken const rows = useMemo(() => fetchRows(), []); const table = useReactTable({ data: rows.filter(r => r.active), // ← new array, every render columns, }); // data changes identity → table re-renders // → .filter() runs again → new array // → data changes identity → ∞
The instinct to memo() your way out is a trap. Material React Table's own guide is blunt: memoMode: 'table-body' disables virtualization and most features, leaving a table "mostly frozen after first render"; memoizing rows costs you access to table state like getIsSelected() [23]. Memoize the data, not the render tree.
Browsers truncate an element's height. MUI documents 17.5M px on Firefox and 33.5M px on Chrome/Edge/Safari [6]; the open TanStack Virtual issue cites 33,554,400 / 33,554,428 [7] ⭐ 7.0k; a third measurement puts Chromium at 16,777,216 [8]. The figure is engine- and version-dependent — budget for the low end, ~16.7M px. Past it the container is silently clipped and the tail of your data is unreachable. The virtualizer does not error. It just stops.
AG Grid's SSRM demo serves 10M records and moves the constraint to your backend [11].
Replace native scrolling; position rows absolutely against a synthetic scroll position [8].
The TanStack Virtual issue is still open [7]. A headless virtualizer ships indices and offsets, not a scrollbar — so this is explicitly your problem [20]. Treat any "millions of rows" claim as marketing unless the grid has replaced native scrolling.
Read this before you trust any number above. Every published 2026 benchmark is vendor-authored, and they do not merely flatter their authors — they contradict each other outright.
| Benchmark | Author | Also ships | Winner | Fixture |
|---|---|---|---|---|
| 1771 Technologies [3] | 1771 Technologies | LyteNyte Grid | LYTENYTE — ITS OWN | not published |
| Battle of the Rows [5] | Revolist OU | RevoGrid | REVOGRID — ITS OWN | not published |
| react-data-grid-benchmark [1] | Vitashev [2] ⭐ 0 | Ace Grid | REACT DATA GRID — NOT ITS OWN | fixture + lockfile + raw samples |
Trust the one that ships a reproducible fixture, a package lock and raw samples — currently only Vitashev's ⭐ 0, which publishes its conflict of interest in the README and still doesn't put its own grid first [2]. And vendor FPS on an M4 MacBook Air [3] tells you nothing about your users' 2019 ThinkPads.
| DOM (+ virtualization) | Canvas — Glide ⭐ 5.3k | |
|---|---|---|
| PAINT AT SCROLL | Create/destroy N nodes per frame — the argument Glide makes against DOM [15] | Repaint dirty rects; flat memory 100 rows → 10M rows [16] |
| HIGH-FREQ UPDATES | Reconciler-bound | Vendor claims 100k+ updates/sec [16] — unverified by any third party |
| TEXT SELECT / CTRL-F | ✓ free | ✗ must be re-implemented [18] |
| ACCESSIBILITY | ✓ real semantics (though pinned columns + virtualization mangle DOM order anyway) | ⚠ hidden mirror DOM; maintainers admit "none of the primary developers are accessibility users so there are likely flaws" [15] |
| CSS / THEMING | ✓ | ✗ draw calls |
| MAINTENANCE | — | LATEST STABLE 6.0.3 · 2024-02-03 [17] |
No stable release in 29 months. Adopting a canvas grid in 2026 means owning a canvas renderer [17]. The practitioner view is also less one-sided than the README: canvas wins scroll and streaming, loses layout primitives, and a well-built DOM grid closes much of the gap [18].
Only relevant if you pair a headless table with a virtualizer — bought grids have one built in and you should not layer another on top. All three hold 60 FPS at 100k items; initial render differs by single-digit milliseconds [19]. It's an API-surface decision.
| Library | Bundle | Latest | Dynamic heights | Verdict |
|---|---|---|---|---|
| @tanstack/react-virtual ⭐ 7.0k | ~5 KB | 3.14.6 · Jul 2026 | measureElement ref |
Default if already on TanStack Table; headless, no markup [20] |
| react-virtuoso ⭐ 6.4k | ~17 KB | 4.18.10 · Jun 2026 | auto · ResizeObserver | Fewest footguns; sticky groups + bidirectional infinite scroll built in |
| react-window ⭐ 17k | ~6 KB | 2.2.7 · Feb 2026 | useDynamicRowHeight |
⚠ Stop repeating the "abandoned, fixed-height-only" line — v2 is a full rewrite [21] |
Virtualization only earns its overhead somewhere around 50+ simultaneously-rendered rows. Column virtualization is where wide grids quietly die: don't bother under ~12–20 columns [24], but note that MUI X disables column virtualization entirely when dynamic row height is on [6] — wide + auto-height means every column mounted.
7.0.0-beta after yearsThe honest proxy for virtualization quality — and the one number no marketing page prints [1].
rowCount × rowHeight vs 16.7M pxA pure-arithmetic go/no-go on native scrolling. Do this one before you write any code [8].
An open MUI X issue leaks ~199 MB per refresh at 100k rows, reaching 4.7 GB [28] ⭐ 5.8k.