←Back to blog
Article

Ten thousand rows in an admin table without freezing the tab

Ten thousand rows in an admin table without freezing the tab
S

StriveBit

4 min readWeb Applications

Ten thousand rows in an admin table without freezing the tab

Picture an operations manager exporting 12,000 shipment records into an admin panel and clicking a column header to sort by dispatch date. The tab hangs for nine seconds on a Redmi Note 9. Somebody sends the screenshot.

The data is already in memory — fetched as JSON, sitting in a Redux slice. The bottleneck is the DOM. React is reconciling 12,000 `<tr>` nodes, the browser is laying them out, and the sort triggers a full re-render. On a desktop Chrome with 16GB RAM this takes 400ms. On a mid-range Android phone with 4GB and Chrome 112, the main thread blocks for long enough that Android shows the "wait or close" dialog.

The first fix is usually not virtualization. It is pagination, and most teams resist it because the product owner wants infinite scroll. Infinite scroll on an admin table is a UX choice that costs you performance, accessibility, and the ability to print. Push back, ship server-side pagination at 50 rows per page, and the freeze disappears. The operations manager can still filter by date range and export the full set to CSV. Nobody needs to see 12,000 rows on one screen.

When a team genuinely needs a long scroll — a reconciliation view where the auditor wants to eyeball consecutive rows without clicking "next page" every five seconds — we reach for windowing. `@tanstack/react-virtual` on top of a flat list, with `overscan` set to 4 rows. The table renders roughly 30 DOM nodes regardless of data size. Sort happens in memory, the virtualizer re-renders only the visible window, and the tab stays responsive.

The implementation is about 40 lines. The hard part is row height. If your rows wrap text, the virtualizer needs a fixed height or a `measureElement` callback. Dynamic measurement works, but it adds a layout pass per row on first scroll. On the Redmi, that first scroll stutters for about 800ms until heights stabilize. We set a fixed `rowHeight` of 48px, truncate long cell content with `text-overflow: ellipsis`, and put the full value in a `title` attribute. The stutter goes away.

Virtualization is not worth it when the table is small enough that the browser can handle it without complaint. We tested this on the Redmi: 500 rows render in under 300ms and scroll smoothly. 1,000 rows take about 1.2 seconds on initial render but scroll fine after that. If your admin view caps at a few hundred rows — and most do, once you add date filters — skip the virtualizer. You are adding a dependency, a measurement step, and a layer of indirection for a problem the browser already solves.

There is a middle ground we use sometimes: render the first 200 rows immediately, then append the rest in chunks of 200 using `requestIdleCallback`. The table appears instantly, and the full set fills in over two or three seconds without blocking interaction. It is not as clean as virtualization, but it avoids the row-height problem entirely and works with existing table markup.

One thing we stopped doing: rendering cells with nested components that each subscribe to a slice. A table with 12,000 rows and eight cells per row means 96,000 component instances. Even if each cell is a simple `<td>`, the reconciliation cost compounds. We flatten to a single `<tr>` per row that reads from a plain object, and we memoize the row component with `React.memo` and a custom comparator that checks the row ID and a `updatedAt` timestamp. If the sort does not change row data, only order, we shuffle keys instead of re-rendering.

Done this way, a 12,000-row reconciliation view renders in a fraction of a second and scrolls smoothly on a phone like the Redmi. The screenshots stop, which is the only metric the operations manager cares about.

←Back to all articles

Ready to build something great?

We help ambitious teams build software that lasts. If you're interested in working with us or want to discuss your project, let's connect.

Get in touch