Benchmark

A hundred thousand rows by twenty columns, measured in your own browser: first render, scroll frames, filter, sort and grouped totals.
Every grid claims to be fast. This page is the claim with an instrument attached: it generates 100,000 rows of 20 columns in your browser, drives the grid through five operations and reports what it measured — on your machine, in your browser, at your window size. The numbers below are from one laptop and are here for scale; the button is the point.

Run it

Rows
Runs

What it measured here

Measured on 2026-09-17 — Windows 11 laptop, 8 cores, Chrome/153.0.8010.48. A figure is the median of 5 runs, each starting from a grid that was mounted a moment earlier, so no operation is scored on what the one before it left behind.
Box Kite 2.0.1 · 100,000 rows × 20 columns · median of 5 · rows built in 155 ms · display frame 7.9 ms
OperationMedianBestWorstBlockingNotes
First render80.6 ms64.8 ms87.1 ms0 ms
Scroll8 ms7.1 ms23.7 ms0 ms15.3 ms of work a frame · 125 fps · worst frame 22.3 ms · 2% under 60 fps · 0% of a hard flick blank
Filter41.8 ms34.1 ms54 ms0 ms
Sort76.4 ms70.4 ms85.9 ms12 ms
Group + aggregate39.2 ms34.2 ms54.3 ms0 ms
The fling is inside a frame, and the work inside it is the number being worked on
The fling is the operation this grid was slowest at — twenty milliseconds of work a frame — because it rendered fifty-eight rows around an eighteen-row viewport. It keeps twelve rows ahead of the scroll and four behind it now, thirty-six in all, and the frame is inside a sixtieth of a second with none of a hard flick blank. What is left is the work inside that frame, which is published rather than left out, and is the number being worked on.

How each number is taken

A measurement starts the moment the change is asked for and stops after the browser has painted it. The frame callback runs before the paint and a task posted from inside it runs after, so what is timed is React's render, the commit, style, layout and paint — not a DOM write with the expensive half still ahead of it. The five state changes are driven by the same code every time, and the rows are generated once and handed to the grid.
OperationWhat the grid is asked to do
First renderMount the grid over rows that are already in memory, and paint the first screen of them.
ScrollFling the rows past at 4,000 pixels a second for two seconds, and report what each frame cost.
FilterFilter one text column down to about a twentieth of the rows.
SortPress the last-name header and sort every row by it.
Group + aggregateGroup by a twelve-value column and total two numeric columns under each group.
The filter is the Japan rows of a twenty-value country column, the sort is a press on the last-name header, and the grouping is department with a total on two money columns and a mean on a third, first level open — so the aggregates are computed over every row rather than over the dozen on screen. The grid is 18 rows tall at 32px a row, with all 20 columns rendered.
Building the rows is charged to nobody
A hundred thousand objects take longer to create than a grid takes to render them. The generator runs once, its time is reported on its own, and no operation includes it.
A slow frame is one that missed 60 fps
The page samples an idle animation-frame loop first and takes the slower of that interval and 16.7 ms as the budget. A frame inside 16.7 ms is smooth on any screen anybody owns, and a headless browser runs its animation loop several times faster than a display does — so the idle interval on its own would score a perfectly smooth scroll as dropping most of it.
A frame interval is quantized, so the scroll is compared on work
Frames arrive on the display's own clock, so a grid that needs 9 ms on a 130 Hz screen misses the 7.7 ms interval and waits for the next one: it reports 15 ms and reads as twice as slow as a grid that needed 7 ms, when the difference between them is two milliseconds. The comparison therefore prints what one frame cost — the same change-to-after-the-paint measurement the other four operations use — and the frames per second beside it, which is what the display allowed. The grid's own table keeps both.
A grid that renders nothing is the fastest grid on the page
A frame time says what the rendering cost, never whether there was anything to render — and a grid that renders nothing is the fastest grid on the page. So a pass of its own flicks the rows past at 10,000 pixels a second — the top of what a hard flick reaches, five rows between one frame and the next at 60 fps — and hit-tests four points down the grid at the start of every frame, against the scroll position that frame is about to paint. The share of frames that found a hole is printed beside the scroll, and a window that renders too few rows ahead of itself shows up there and in no other number on this page.

What it does not measure

A benchmark that only flatters the thing it measures is an advertisement. Four things this one leaves out, each of which could change the answer for a particular application.
A canvas grid wins the numbers a canvas grid is built for
A grid that draws its cells into a bitmap rather than creating elements has a scroll cost close to flat however many columns are on screen, and nothing here will beat it at that. What it pays for it is everything a DOM makes free: text selection, find-in-page, a screen reader, a browser extension, a CSS rule, an element inspector. This grid is a DOM grid and is measured as one.
The scroll is scripted, not thrown by a finger
The scroll scenario sets scrollTop on every animation frame, at a speed taken from the clock rather than a fixed number of pixels — so a frame that took 100 ms to render lands 400 px further down, the way a real fling does. What it exercises is the virtualization and the re-render, not the compositor path a wheel or a touch drag also takes.
Every one of the twenty columns is rendered
The grid is left at its own defaults apart from the geometry — twenty columns at the same widths, 32px rows, a 40px header — and it virtualizes rows but not columns, so all twenty are in the DOM. A grid that renders only the columns on screen is doing roughly a third of the cell work on a page narrower than the table, which is worth knowing before reading any figure here as an absolute.
The rows are already in memory
Nothing here crosses a network. A grid whose rows arrive from a server a block at a time is a different measurement, and the thing that dominates it is the server.
One machine, one browser, one window
Every figure on this page moves with the hardware, the browser, the window size and whatever else the machine is doing. That is why the run button is here rather than a table of numbers you have to take on trust.

Rerunning it, and in CI

The page is the benchmark, so the harness is the same code either way: npm run bench builds the site, serves it, drives this page in headless Chrome and writes what it measured to bench/results.json — the file the table above reads.
From a clone of the repository
Terminal
npm run build:pages
npm run bench -- --runs 5

# or against a server you are already running
npm run bench -- --url http://localhost:4173/benchmark/
The same command runs in CI against a budget file, so a change that makes the grid an order of magnitude slower fails the build rather than being noticed a release later. The budgets are sized for a shared CI runner, which is several times slower than any laptop — they catch a regression, not a drift.