Infinite Table vs AG Grid, benchmarked
300,000 and 1,000,000 rows, the same data and the same interactions in both grids, measured in Chrome. Open source and reproducible: run it on your own machine.
Run 2026-11-11-clean on November 11, 2026 · AG Grid Community 36.3.0 · Infinite Table 10.1.4
Results
Medians of 7 runs per grid and scenario. Where the 95% confidence interval includes "no difference", we call it a tie. Every chart has a table view, and every scenario has a table with all its metrics.
Mount
From setState with all rows already in memory to the first frame that shows the whole viewport correctly, and until the grid stops changing the DOM.
View as table
| Mount: first rows painted | AG Grid | Infinite Table |
|---|---|---|
| 300k rows | 198 ms | 140 ms |
| 1M rows | 498 ms | 352 ms |
View as table
| Mount: until settled | AG Grid | Infinite Table |
|---|---|---|
| 300k rows | 302 ms | 140 ms |
| 1M rows | 602 ms | 352 ms |
View as table
| JS heap retained by the grid | AG Grid | Infinite Table |
|---|---|---|
| 300k rows | 67.5 MB | 36.6 MB |
| 1M rows | 213 MB | 90.7 MB |
Mount, 300k rows: every metric
Mount the grid with all the rows (data already in memory). "Visible" = from setState to the end of the first frame committed with the whole viewport (first/last visible row and column) showing the right content. "Settled" = until the first frame committed after the grid's last DOM mutation. Memory is the JS heap retained by the grid after GC (heap after mount - heap before mount).
| Metric | AG Grid | Infinite Table | Infinite / AG | Better |
|---|---|---|---|---|
| Time to first rows paintedlower is better | 198 ms | 140 ms | 0.71×95% CI 0.64–0.78 | Infinite Table |
| Time until settledlower is better | 302 ms | 140 ms | 0.46×95% CI 0.43–0.51 | Infinite Table |
| Main thread: scriptinglower is better | 180 ms | 126 ms | 0.70×95% CI 0.64–0.80 | Infinite Table |
| Main thread: layoutlower is better | 7.64 ms | 2.33 ms | 0.31×95% CI 0.28–0.34 | Infinite Table |
| Main thread: style recalclower is better | 7.76 ms | 3.82 ms | 0.49×95% CI 0.46–0.55 | Infinite Table |
| Main thread: all taskslower is better | 205 ms | 145 ms | 0.71×95% CI 0.65–0.77 | Infinite Table |
| JS heap retained by gridlower is better | 67.5 MB | 36.6 MB | 0.54×95% CI 0.54–0.54 | Infinite Table |
| DOM elements in gridlower is better | 637 | 609 | 0.96×95% CI 0.96–0.96 | Infinite Table |
Mount, 1M rows: every metric
Mount the grid with all the rows (data already in memory). "Visible" = from setState to the end of the first frame committed with the whole viewport (first/last visible row and column) showing the right content. "Settled" = until the first frame committed after the grid's last DOM mutation. Memory is the JS heap retained by the grid after GC (heap after mount - heap before mount).
| Metric | AG Grid | Infinite Table | Infinite / AG | Better |
|---|---|---|---|---|
| Time to first rows paintedlower is better | 498 ms | 352 ms | 0.71×95% CI 0.57–0.79 | Infinite Table |
| Time until settledlower is better | 602 ms | 352 ms | 0.59×95% CI 0.48–0.65 | Infinite Table |
| Main thread: scriptinglower is better | 480 ms | 342 ms | 0.71×95% CI 0.57–0.80 | Infinite Table |
| Main thread: layoutlower is better | 7.13 ms | 2.13 ms | 0.30×95% CI 0.28–0.31 | Infinite Table |
| Main thread: style recalclower is better | 7.84 ms | 3.62 ms | 0.46×95% CI 0.43–0.50 | Infinite Table |
| Main thread: all taskslower is better | 510 ms | 355 ms | 0.70×95% CI 0.57–0.78 | Infinite Table |
| JS heap retained by gridlower is better | 213 MB | 90.7 MB | 0.42×95% CI 0.42–0.42 | Infinite Table |
| DOM elements in gridlower is better | 637 | 609 | 0.96×95% CI 0.96–0.96 | Infinite Table |
Sorting
Click a column header, from pointerdown to the first frame with every visible row sorted.
View as table
| Sort a string column (ascending) | AG Grid | Infinite Table |
|---|---|---|
| 300k rows | 774 ms | 348 ms |
| 1M rows | 3,384 ms | 1,395 ms |
View as table
| Sort a number column (ascending) | AG Grid | Infinite Table |
|---|---|---|
| 300k rows | 707 ms | 306 ms |
| 1M rows | 3,061 ms | 1,205 ms |
Sort a string column, 300k rows: every metric
Click the "code" column header (string column, unique-ish values) - first click sorts ascending (cold), second click descending. Measured from pointerdown to the end of the first frame committed with all visible rows sorted (and each row consistent: its cells belong to the same data row).
| Metric | AG Grid | Infinite Table | Infinite / AG | Better |
|---|---|---|---|---|
| Ascending: click to sorted rows paintedlower is better | 774 ms | 348 ms | 0.45×95% CI 0.38–0.46 | Infinite Table |
| Ascending: click to settledlower is better | 774 ms | 362 ms | 0.47×95% CI 0.39–0.48 | Infinite Table |
| Descending: click to sorted rows paintedlower is better | 810 ms | 360 ms | 0.44×95% CI 0.40–0.49 | Infinite Table |
| Descending: click to settledlower is better | 810 ms | 360 ms | 0.44×95% CI 0.40–0.49 | Infinite Table |
| Ascending: main thread scriptinglower is better | 762 ms | 335 ms | 0.44×95% CI 0.37–0.45 | Infinite Table |
| Descending: main thread scriptinglower is better | 801 ms | 347 ms | 0.43×95% CI 0.39–0.48 | Infinite Table |
Sort a number column, 300k rows: every metric
Click the "salary" column header (number column, unique-ish values) - first click sorts ascending (cold), second click descending. Measured from pointerdown to the end of the first frame committed with all visible rows sorted (and each row consistent: its cells belong to the same data row).
| Metric | AG Grid | Infinite Table | Infinite / AG | Better |
|---|---|---|---|---|
| Ascending: click to sorted rows paintedlower is better | 707 ms | 306 ms | 0.43×95% CI 0.38–0.45 | Infinite Table |
| Ascending: click to settledlower is better | 707 ms | 312 ms | 0.44×95% CI 0.39–0.48 | Infinite Table |
| Descending: click to sorted rows paintedlower is better | 714 ms | 314 ms | 0.44×95% CI 0.37–0.48 | Infinite Table |
| Descending: click to settledlower is better | 714 ms | 314 ms | 0.44×95% CI 0.37–0.48 | Infinite Table |
| Ascending: main thread scriptinglower is better | 695 ms | 295 ms | 0.42×95% CI 0.37–0.45 | Infinite Table |
| Descending: main thread scriptinglower is better | 705 ms | 305 ms | 0.43×95% CI 0.36–0.48 | Infinite Table |
Sort a string column, 1M rows: every metric
Click the "code" column header (string column, unique-ish values) - first click sorts ascending (cold), second click descending. Measured from pointerdown to the end of the first frame committed with all visible rows sorted (and each row consistent: its cells belong to the same data row).
| Metric | AG Grid | Infinite Table | Infinite / AG | Better |
|---|---|---|---|---|
| Ascending: click to sorted rows paintedlower is better | 3,384 ms | 1,395 ms | 0.41×95% CI 0.37–0.44 | Infinite Table |
| Ascending: click to settledlower is better | 3,384 ms | 1,412 ms | 0.42×95% CI 0.38–0.44 | Infinite Table |
| Descending: click to sorted rows paintedlower is better | 3,279 ms | 1,380 ms | 0.42×95% CI 0.38–0.45 | Infinite Table |
| Descending: click to settledlower is better | 3,279 ms | 1,380 ms | 0.42×95% CI 0.38–0.45 | Infinite Table |
| Ascending: main thread scriptinglower is better | 3,373 ms | 1,383 ms | 0.41×95% CI 0.37–0.43 | Infinite Table |
| Descending: main thread scriptinglower is better | 3,271 ms | 1,366 ms | 0.42×95% CI 0.38–0.45 | Infinite Table |
Sort a number column, 1M rows: every metric
Click the "salary" column header (number column, unique-ish values) - first click sorts ascending (cold), second click descending. Measured from pointerdown to the end of the first frame committed with all visible rows sorted (and each row consistent: its cells belong to the same data row).
| Metric | AG Grid | Infinite Table | Infinite / AG | Better |
|---|---|---|---|---|
| Ascending: click to sorted rows paintedlower is better | 3,061 ms | 1,205 ms | 0.39×95% CI 0.34–0.47 | Infinite Table |
| Ascending: click to settledlower is better | 3,061 ms | 1,213 ms | 0.40×95% CI 0.34–0.47 | Infinite Table |
| Descending: click to sorted rows paintedlower is better | 3,096 ms | 1,289 ms | 0.42×95% CI 0.40–0.44 | Infinite Table |
| Descending: click to settledlower is better | 3,096 ms | 1,289 ms | 0.42×95% CI 0.40–0.44 | Infinite Table |
| Ascending: main thread scriptinglower is better | 3,049 ms | 1,193 ms | 0.39×95% CI 0.34–0.46 | Infinite Table |
| Descending: main thread scriptinglower is better | 3,088 ms | 1,275 ms | 0.41×95% CI 0.39–0.44 | Infinite Table |
Jumping through the dataset
Ten programmatic jumps to far-away positions, like dragging the scrollbar thumb.
View as table
| Jump to rows painted | AG Grid | Infinite Table |
|---|---|---|
| 300k rows (no significant difference) | 21 ms | 27.1 ms |
| 1M rows | 20.9 ms | 23.6 ms |
Jump through the dataset, 300k rows: every metric
Programmatic jumps to 10 far-away scroll positions (like dragging the scrollbar thumb). Measured from setting scrollTop to the end of the first frame committed with the first and last visible rows showing the right content, at the right position.
| Metric | AG Grid | Infinite Table | Infinite / AG | Better |
|---|---|---|---|---|
| Jump to rows painted (median of jumps)lower is better | 21 ms | 27.1 ms | 1.29×95% CI 0.90–1.47 | no significant difference |
| Jump to rows painted (worst jump)lower is better | 39.9 ms | 36.7 ms | 0.92×95% CI 0.72–1.18 | no significant difference |
| Jump to settled (median of jumps)lower is better | 21 ms | 27.1 ms | 1.29×95% CI 0.90–1.47 | no significant difference |
| Main thread scripting (all jumps)lower is better | 133 ms | 163 ms | 1.22×95% CI 0.96–1.36 | no significant difference |
Jump through the dataset, 1M rows: every metric
Programmatic jumps to 10 far-away scroll positions (like dragging the scrollbar thumb). Measured from setting scrollTop to the end of the first frame committed with the first and last visible rows showing the right content, at the right position.
| Metric | AG Grid | Infinite Table | Infinite / AG | Better |
|---|---|---|---|---|
| Jump to rows painted (median of jumps)lower is better | 20.9 ms | 23.6 ms | 1.13×95% CI 1.01–1.27 | AG Grid |
| Jump to rows painted (worst jump)lower is better | 35.6 ms | 28.3 ms | 0.79×95% CI 0.70–0.84 | Infinite Table |
| Jump to settled (median of jumps)lower is better | 20.9 ms | 23.6 ms | 1.13×95% CI 1.01–1.27 | AG Grid |
| Main thread scripting (all jumps)lower is better | 127 ms | 143 ms | 1.12×95% CI 0.98–1.17 | no significant difference |
Scrolling
A wheel gesture through Chrome's real input pipeline: 24,000 px down at 8,000 px/s, and four horizontal sweeps across all columns at 4,000 px/s. At 300k rows both grids hold 60 fps, with no dropped frames and no long animation frames; at 1M rows both stay at 59-60 fps, with no significant difference. Where they differ is how much main-thread work it takes:
View as table
| Vertical scrolling, 300k rows: main-thread work | AG Grid | Infinite Table |
|---|---|---|
| scripting | 398 ms | 474 ms |
| layout | 78.3 ms | 53.7 ms |
| style recalc | 172 ms | 122 ms |
| all tasks (no significant difference) | 1,020 ms | 1,029 ms |
View as table
| Horizontal scrolling, 300k rows: main-thread work | AG Grid | Infinite Table |
|---|---|---|
| scripting | 171 ms | 108 ms |
| layout | 48.8 ms | 8.2 ms |
| style recalc | 30.6 ms | 34.1 ms |
| all tasks | 414 ms | 317 ms |
Scroll vertically, 300k rows: every metric
Wheel-scroll down 24000px at 8000px/s (Input.synthesizeScrollGesture - real input pipeline, same gesture for both grids). Frame outcomes come from Chrome's compositor (PipelineReporter): "partial" = frame shown without the main thread's update, "dropped" = no new frame. rAF frame intervals and Long Animation Frames come from the page.
| Metric | AG Grid | Infinite Table | Infinite / AG | Better |
|---|---|---|---|---|
| Frames fully presentedhigher is better | 52.3% | 56% | 1.07×95% CI 0.99–1.14 | no significant difference |
| Frames presented without main-thread updatelower is better | 47.7% | 44% | 0.92×95% CI 0.84–1.02 | no significant difference |
| Frames droppedlower is better | 0% | 0% | 0%95% CI 0% to 0% | no significant difference |
| Main thread frame rate (rAF)higher is better | 60 fps | 59.9 fps | 1.00×95% CI 0.99–1.00 | no significant difference |
| p95 rAF frame intervallower is better | 17.5 ms | 17.5 ms | 1.00×95% CI 0.99–1.01 | no significant difference |
| Worst rAF frame intervallower is better | 17.7 ms | 17.7 ms | 1.00×95% CI 1.00–1.90 | no significant difference |
| rAF intervals > 25mslower is better | 0% | 0% | 0%95% CI 0% to 0.54% | no significant difference |
| Long animation frames: total blockinglower is better | 0 ms | 0 ms | 0 ms95% CI 0 ms to 0 ms | no significant difference |
| Main thread: scriptinglower is better | 398 ms | 474 ms | 1.19×95% CI 1.10–1.32 | AG Grid |
| Main thread: layoutlower is better | 78.3 ms | 53.7 ms | 0.68×95% CI 0.60–0.78 | Infinite Table |
| Main thread: style recalclower is better | 172 ms | 122 ms | 0.71×95% CI 0.65–0.76 | Infinite Table |
| Main thread: all taskslower is better | 1,020 ms | 1,029 ms | 1.01×95% CI 0.89–1.11 | no significant difference |
Scroll horizontally, 300k rows: every metric
Wheel-scroll right/left over all columns, 4 sweeps at 4000px/s (Input.synthesizeScrollGesture - real input pipeline, same gesture for both grids). Frame outcomes come from Chrome's compositor (PipelineReporter): "partial" = frame shown without the main thread's update, "dropped" = no new frame. rAF frame intervals and Long Animation Frames come from the page.
| Metric | AG Grid | Infinite Table | Infinite / AG | Better |
|---|---|---|---|---|
| Frames fully presentedhigher is better | 88.9% | 95.8% | 1.08×95% CI 0.94–1.12 | no significant difference |
| Frames presented without main-thread updatelower is better | 11.1% | 4.23% | 0.38×95% CI 0.26–1.93 | no significant difference |
| Frames droppedlower is better | 0% | 0% | 0%95% CI 0% to 0% | no significant difference |
| Main thread frame rate (rAF)higher is better | 60 fps | 60 fps | 1.00×95% CI 1.00–1.00 | no significant difference |
| p95 rAF frame intervallower is better | 17.3 ms | 17.2 ms | 0.99×95% CI 0.98–1.01 | no significant difference |
| Worst rAF frame intervallower is better | 17.7 ms | 17.6 ms | 0.99×95% CI 0.99–1.01 | no significant difference |
| rAF intervals > 25mslower is better | 0% | 0% | 0%95% CI 0% to 0% | no significant difference |
| Long animation frames: total blockinglower is better | 0 ms | 0 ms | 0 ms95% CI 0 ms to 0 ms | no significant difference |
| Main thread: scriptinglower is better | 171 ms | 108 ms | 0.63×95% CI 0.60–0.70 | Infinite Table |
| Main thread: layoutlower is better | 48.8 ms | 8.2 ms | 0.17×95% CI 0.16–0.18 | Infinite Table |
| Main thread: style recalclower is better | 30.6 ms | 34.1 ms | 1.11×95% CI 1.00–1.17 | AG Grid |
| Main thread: all taskslower is better | 414 ms | 317 ms | 0.77×95% CI 0.73–0.80 | Infinite Table |
Scroll vertically, 1M rows: every metric
Wheel-scroll down 24000px at 8000px/s (Input.synthesizeScrollGesture - real input pipeline, same gesture for both grids). Frame outcomes come from Chrome's compositor (PipelineReporter): "partial" = frame shown without the main thread's update, "dropped" = no new frame. rAF frame intervals and Long Animation Frames come from the page.
| Metric | AG Grid | Infinite Table | Infinite / AG | Better |
|---|---|---|---|---|
| Frames fully presentedhigher is better | 52.3% | 58.5% | 1.12×95% CI 1.03–1.14 | Infinite Table |
| Frames presented without main-thread updatelower is better | 47.7% | 41.4% | 0.87×95% CI 0.86–0.96 | Infinite Table |
| Frames droppedlower is better | 0% | 0% | 0%95% CI -0.56% to 0.55% | no significant difference |
| Main thread frame rate (rAF)higher is better | 59.9 fps | 59.7 fps | 1.00×95% CI 0.99–1.02 | no significant difference |
| p95 rAF frame intervallower is better | 17.5 ms | 17.5 ms | 1.00×95% CI 1.00–1.02 | no significant difference |
| Worst rAF frame intervallower is better | 17.7 ms | 33.5 ms | 1.89×95% CI 0.27–1.92 | no significant difference |
| rAF intervals > 25mslower is better | 0% | 0.54% | +0.54%95% CI -0.55% to 1.09% | no significant difference |
| Long animation frames: total blockinglower is better | 0 ms | 0 ms | 0 ms95% CI 0 ms to 0 ms | no significant difference |
| Main thread: scriptinglower is better | 502 ms | 593 ms | 1.18×95% CI 1.11–1.26 | AG Grid |
| Main thread: layoutlower is better | 86.6 ms | 69.6 ms | 0.80×95% CI 0.74–0.88 | Infinite Table |
| Main thread: style recalclower is better | 274 ms | 153 ms | 0.56×95% CI 0.52–0.59 | Infinite Table |
| Main thread: all taskslower is better | 1,319 ms | 1,269 ms | 0.96×95% CI 0.89–1.00 | Infinite Table |
Scroll horizontally, 1M rows: every metric
Wheel-scroll right/left over all columns, 4 sweeps at 4000px/s (Input.synthesizeScrollGesture - real input pipeline, same gesture for both grids). Frame outcomes come from Chrome's compositor (PipelineReporter): "partial" = frame shown without the main thread's update, "dropped" = no new frame. rAF frame intervals and Long Animation Frames come from the page.
| Metric | AG Grid | Infinite Table | Infinite / AG | Better |
|---|---|---|---|---|
| Frames fully presentedhigher is better | 86.3% | 89% | 1.03×95% CI 0.94–1.04 | no significant difference |
| Frames presented without main-thread updatelower is better | 13.7% | 11% | 0.81×95% CI 0.75–1.64 | no significant difference |
| Frames droppedlower is better | 0% | 0% | 0%95% CI 0% to 0% | no significant difference |
| Main thread frame rate (rAF)higher is better | 60 fps | 60 fps | 1.00×95% CI 1.00–1.00 | no significant difference |
| p95 rAF frame intervallower is better | 17.4 ms | 17.4 ms | 1.00×95% CI 0.99–1.01 | no significant difference |
| Worst rAF frame intervallower is better | 17.7 ms | 17.6 ms | 0.99×95% CI 0.99–1.01 | no significant difference |
| rAF intervals > 25mslower is better | 0% | 0% | 0%95% CI 0% to 0% | no significant difference |
| Long animation frames: total blockinglower is better | 0 ms | 0 ms | 0 ms95% CI 0 ms to 0 ms | no significant difference |
| Main thread: scriptinglower is better | 165 ms | 111 ms | 0.67×95% CI 0.61–0.75 | Infinite Table |
| Main thread: layoutlower is better | 51.5 ms | 8.79 ms | 0.17×95% CI 0.16–0.19 | Infinite Table |
| Main thread: style recalclower is better | 30.3 ms | 35.4 ms | 1.17×95% CI 1.00–1.30 | no significant difference |
| Main thread: all taskslower is better | 402 ms | 353 ms | 0.88×95% CI 0.71–0.94 | Infinite Table |
What you see during a very fast scroll
When you flick the wheel faster than any grid can re-render, a grid has to show something for the rows it hasn't caught up with. AG Grid shows empty space until the right content is rendered. Infinite Table moves the existing rows into place immediately and updates their content on the next render, so strong inertial scrolling never reveals blank areas, but for a frame or two some rows can show the content they held before. For horizontal scrolling it waits for the render.
The benchmark measures both effects during a 20,000 px/s flick. Those numbers are in the full report.
Method
Setup
- The same deterministic data in both grids: 20 columns, at 300,000 and 1,000,000 rows, with a checksum verified before every run.
- Same row height, header height and column widths. Otherwise each grid runs on its defaults. AG Grid uses the Quartz theme,
animateRows={false}and agetRowId. - Production builds only.
- 7 measured runs per grid and scenario, plus 1 discarded warmup, interleaved A/B, a fresh browser context for every run.
How we measure
- Latency comes from Chrome performance traces, as in js-framework-benchmark: from the input event to the end of the first frame where the DOM shows the expected rows and columns.
- Scrolling uses
Input.synthesizeScrollGesture, so both grids get the same real wheel input. Frame outcomes come from Chrome's compositor. - Medians, with a seeded 95% bootstrap confidence interval of the ratio between the grids. We only call a winner when the interval excludes "no difference".
Machine and browser
| Machine | Apple M1 Pro × 10, Darwin 24.1.0 arm64 |
|---|---|
| Browser | Chrome 136.0.7103.25, headless, sandboxed, display scale 1 |
| CPU throttling | 1× (none) |
| Viewport | 1400×1000 |
| Runs | 7 measured + 1 warmup per grid and scenario |
| Versions | AG Grid Community 36.3.0, Infinite Table 10.1.4 (built from source) |
| Run | 2026-11-11-clean, November 11, 2026 |
Limitations
- One machine, one browser. A fast laptop without CPU throttling. Compare the ratios between the grids, not the absolute numbers.
- Headless Chrome. It runs the same rendering code as headed Chrome, but frame pacing can differ with a real display.
- Defaults only. Both grids have options that could change these results. We didn't tune either.
- One dataset shape. Plain text and numbers: no custom cell renderers, grouping, filtering or live updates.
- We have an interest. We make Infinite Table. That is why everything here is open, and why you can run it yourself.
Run it yourself
The benchmark lives in examples/src/benchmark in the Infinite Table repository. You need git, Node.js (we used 22) and about 2.5 GB of disk space, on macOS or Linux. Playwright downloads its own Chromium.
git clone https://github.com/infinite-table/infinite-react.git
cd infinite-react
# three installs: shared tooling, the grid's source, the examples app
npm ci
npm ci --prefix source
npm ci --prefix examples
cd examples
npx playwright install chromium
# production build, then every scenario at 300k and 1M rows (about 25 minutes)
BENCH_HEADLESS=1 npm run benchmark
# or a one-minute taste: mount and string sorting, 300k rows, 3 runs
BENCH_HEADLESS=1 BENCH_SIZES=300k BENCH_SCENARIOS=mount,sort-string BENCH_RUNS=3 npm run benchmark:runEach run writes report.md (medians, ratios, confidence intervals) and results.json (every raw sample) to examples/benchmark-results/<run id>/. The README documents every option and scenario.
Help us make it fairer
If you know a setting that makes either grid faster, or a scenario we should add, please open an issue or a pull request. We want the benchmark to be fair to both grids.