Every React app eventually has to show 50,000 rows of something — transactions, audit logs, leads, orders, sensor readings. And every team eventually discovers that React's "just .map() over the array" advice doesn't scale past about 5,000. At 50,000 the page locks for 14 seconds, scrolling is 8fps, and the tab burns 1.2 GB.
This is a condensed walkthrough. I took a real 250,000-row fintech transactions dashboard and benchmarked every relevant technique on the same dataset (TTI, scroll FPS, INP, DOM nodes, memory). The full guide with all the code, the complete comparison matrix, the decision flow, and the production architecture is on my site 👇
Why naive rendering breaks
rows.map(r => <tr>…</tr>) over 250k rows breaks in order: reconciliation diffs all 250k elements (~3–5s of main-thread work), the DOM balloons to ~750k nodes, every row holds a fiber + listeners (1+ GB), and TTI shoots past 10 seconds. The fix isn't one technique — it's the right technique for your data shape and UX.
The five techniques, same 250k-row dataset (4× CPU throttle)
1. Server-side pagination — the cheapest 10× you'll ever get. Only ship what the user can see; run filters/sorts on the server. Use cursor pagination, not offset — at row 50,000, OFFSET 50000 makes the DB walk 50k rows it then throws away. Alone: TTI 14.2s → 0.8s, memory 1.2 GB → 95 MB. If you do nothing else, do this.
2. Infinite scroll — a UX choice, not a perf one. Right for feeds/consumption; wrong for audit/analyst grids where users need page numbers, jump-to-row, and shareable URLs. And it's a trap without virtualization — after 100 pages the DOM hits 15,000+ nodes and FPS drops to 22.
3. Virtual scrolling — the biggest client-side win. Render only the ~30 rows the user can see; the container lies about being tall enough for all 250k. DOM nodes 750k → ~120, FPS 8 → 60. But it doesn't fix memory if the data's still in JS — pair it with pagination.
4. AG Grid — the enterprise default. Need grouping, pivots, master-detail, Excel-like editing, server-side row model? AG Grid gives it all out of the box (Community is free/MIT and excellent). Cost: ~270 KB gzipped and opinionated theming.
5. TanStack Table — headless, write your own DOM. It owns sort/filter/paginate state; you render the markup. Pair with @tanstack/react-virtual and you get an AG-Grid-fast custom grid for ~+20 KB. The right pick for a design system.
The comparison, at a glance
| Metric | Naive | Server pagination | Virtual only | AG Grid SSRM | TanStack + Virtual |
|---|---|---|---|---|---|
| TTI | 14.2s | 0.8s | 1.4s | 0.5s | 0.7s |
| Scroll FPS | 8 | 60 | 60 | 60 | 60 |
| DOM nodes | 750k | 250 | ~120 | ~200 | ~140 |
| Memory | 1.2 GB | 95 MB | 380 MB | 110 MB | 90 MB |
| Bundle | 0 | 0 | +6 KB | +270 KB | +20 KB |
What we actually shipped
Server cursor pagination + virtual scrolling + TanStack Table (headless) for the design system. Same fintech app: 14.2s → 0.6s TTI · 8 → 60fps · 1.2 GB → 110 MB, and "the grid is slow" support tickets went 14/month → 0.
The mental model
The answer to "show me 250,000 rows" is almost never a single library — it's a combination. Server pagination first, always. Virtualize when any page renders more than ~200 rows. Then pick the library by feature need, not vibes. Do that and "make the table faster" stops being a recurring ticket and becomes a one-time architecture choice.
The full guide has the real code for all five techniques (TanStack Query cursor pagination, IntersectionObserver infinite scroll, @tanstack/react-virtual, AG Grid server-side row model, TanStack Table), the full matrix, the decision flow, and the production architecture diagram:
Originally published on PrepStack.
Top comments (0)