Skip to content

Repository files navigation

React Data Grid Benchmark

Read Data Grid Benchmarking

A detailed guide for benchmarking the performance of well-known React data grids. This README covers configurations, testing methods, and the rationale for evaluating rendering performance across scrolling, sorting, filtering, and dataset replacement.

Note

The README explains how to set up the testing environment and run the benchmarks locally. Local benchmark results can vary depending on your machine, browser, and system load. For performance results from our testing environment, see this published article.

The approach used for this benchmark was inspired by the JS Framework Benchmark.

Purpose and Scope

We want to measure how quickly various React Data Grids can go from an action to pixels on the screen. For the purposes of these benchmarks an action can be scrolling the grid, replacing the grid's dataset, filtering, or sorting.

Data Grids offer a wide variety of features and capabilities, many of which are unrelated to performance. This benchmark focuses on rendering performance, so features such as cell editing and row selection are not included in the measured actions.

While this benchmark does not cover every available library or test every performance scenario, it focuses, in our judgment, on the performance characteristics that matter when assessing React data grid behavior.

Apart from render performance, we assess the bundle sizes of the data grids in our benchmarks. To measure this, we use esbuild to bundle the code for each grid, and then gzip it using Node's zlib library. The size of the resulting file is then reported in kilobytes, giving us a good sense of the overall efficiency.

We are the creators of LyteNyte Grid, which is a data grid included in these benchmarks. Whilst we have made every effort to ensure the fairness of these benchmarks, we encourage developers to perform their own due diligence, and measure performance for their own specific use cases.

Quick Start

Follow these steps to get started:

  1. Clone the repository
  2. Change to the benchmarks directory:
    cd benchmarks-grid
  3. Install all the dependencies:
    pnpm install
  4. Generate the benchmark data:
    pnpm run generate:data
  5. Build the benchmark app for production:
    pnpm run build
  6. Start a local production preview (the preview runs on port 4173):
    pnpm run preview
  7. Run the benchmark with tsx. For example:
    pnpm tsx ./bench/01-scroll-10k.ts

To run the bundle size checks, only the install and build steps are required; the preview server does not need to be running.

pnpm tsx ./check-bundle-size.ts`

Prerequisites

Before you run the benchmarks, ensure your testing environment includes:

Recommended Test Environment

The benchmarks have been tested on Windows 10 or later and on macOS Sequoia. Other operating systems may work if they support Node.js and Google Chrome, but we can’t guarantee compatibility.

By nature, these benchmarks are rendering-heavy, so having GPU-accelerated hardware is recommended. On macOS, use a Mac with an Apple M3 chip or newer.

For the most stable results, ensure no nonessential background processes are running before running the benchmarks. The reference benchmark run requires Chrome in headed mode, so a physical or virtual display must be connected.

Note

The benchmarks take approximately 8 hours to run, but may take significantly longer on less powerful hardware.

Repository Layout

The repository contains a Vite-powered React application. We use Vite to build the production version of the application. For development, the Vite dev server can also be used by running:

pnpm run dev

All the code for the benchmarks is kept in the src folder.

  • components: Contains a div wrapper for rendering each data grid. This is necessary as some data grids require a container with an intrinsic size before they can render correctly.

  • routes: Contains the test implementation for each data grid. Each test has a numbered folder, and each numbered folder contains one folder per data grid with the corresponding test code.

In addition, the following folders are also used:

  • public: Contains test assets, and in particular pre-computed test data that can be used for the various tests. The precomputed data exists to ensure that no test spends time creating data.
  • patches: Fixes an export issue present in Handsontable. See this issue for more details. The patch has no impact on Handsontable's performance in these benchmarks.
  • pages: Contains an HTML file for each test. Tests are isolated into their own route in the production build to prevent cross-contamination of results.
  • bundle-checks: Contains the entry points used by the check-bundle-sizes script to determine the minimum and maximum bundle sizes for the different grids.
  • bench: Contains the benchmark test scripts that are run for each grid. Each script is a test that is measured as part of the benchmark results. You can run any script using tsx, for example, pnpm tsx ./bench/02-scroll-200k.ts.

Note

The test scripts run server-side. Before running any test script, ensure the preview instance is running. See the Quick Start steps for more details.

Benchmark Methodology

Each benchmark script in the bench folder is run using Measure Right, a benchmarking library built on top of Playwright.

Measure Right launches Chrome, opens the relevant test page, and performs the interaction defined in the benchmark, such as scrolling.

Each benchmark includes the following steps:

  • before: Navigates to the page and waits until the required elements are visible.
  • warm: Runs unrecorded warm-up iterations.
  • run: Runs the measured interaction.

Browser Configuration

Measure Right runs each test in a headed Chrome instance so the full GPU compositing pipeline remains active. This setup helps ensure the benchmarks reflect realistic rendering conditions, including:

  • Hardware-accelerated CSS transforms
  • Compositor layers
  • GPU rasterization
  • Layout and paint timing that avoid software-rendering fallback

The benchmark runner always launches Chrome with these flags:

  1. --js-flags=--expose-gc: Enables the window.gc() call used to force garbage collection before each pass.
  2. --enable-benchmarking: Enables additional Chrome benchmarking APIs.

Measure Right uses the system-installed Google Chrome rather than Playwright’s bundled Chromium. It resolves the Chrome binary from the standard installation path for each platform:

  • macOS: /Applications/Google Chrome.app
  • Linux: /usr/bin/google-chrome
  • Windows: C:\Program Files\Google\Chrome\Application\chrome.exe

Since the benchmarks use the installed Chrome version, the results are tied to whichever Chrome version is installed at the time of the run. When sharing or comparing results, always record the Chrome version (chrome://version) along with the hardware specification.

Scroll Pacing and Frame Synchronisation

Scroll benchmarks use a global Scroll helper defined as an inline <script> tag in each test page’s HTML file.

The helper calls the browser’s native scrollBy method, but it does not apply the full scroll distance in a single step. Instead, it:

  • Divides the total scroll distance into fixed 500px steps.
  • Wraps each scrollBy call in requestAnimationFrame.
  • Waits for one frame boundary between each step.

For example, a 2,000px scroll produces four 500px increments, with one requestAnimationFrame pause between each increment.

Each increment is issued only after the previous frame has been committed. The grid must complete its rendering work within the current frame before the next scroll position is applied.

The measured duration reflects the grid’s frame-by-frame rendering throughput, not how quickly the test script can dispatch events. A grid that falls behind the frame budget will stall at each requestAnimationFrame boundary, naturally producing a longer measured duration.

Two-Pass Measurement

For each benchmark iteration, Measure Right performs two separate browser runs.

Memory Pass

The memory pass uses the Chrome DevTools Protocol (CDP) Performance.getMetrics API to capture JSHeapUsedSize in megabytes after the benchmark completes. This isolates the JavaScript heap memory consumed by the grid during the interaction.

CPU/Timing Pass

The CPU/timing pass enables Chrome tracing across the following categories:

  • blink.user_timing
  • devtools.timeline
  • disabled-by-default-devtools.timeline

Measure Right brackets the benchmark run function with performance.mark("bench_start") and performance.mark("bench_end") to define the measurement window. The resulting trace file is parsed to extract the timing metrics described below.

Before each pass, Measure Right forces a synchronous major garbage collection:

await page.evaluate(
  "window.gc({type:'major',execution:'sync',flavor:'last-resort'})",
);

This creates a clean heap baseline and prevents previous runs from contaminating the results.

Warm Up and Iterations

Before any measurements are recorded, each benchmark is run 5 times as a warm-up run.

These warm-up rounds are discarded. They exist solely to:

  • Allow the browser’s JIT compiler to reach a steady optimization state.
  • Eliminate cold-start effects from the measured results, such as initial script parsing, layout, and paint.

Following the warm-up, each benchmark is measured across 50 iterations.

Running 50 iterations provides a sufficiently large sample to produce stable statistical estimates and to surface outliers caused by background OS activity or transient CPU throttling.

Metrics and Trace Analysis

During the CPU/timing pass, Playwright instructs Chrome to record an execution trace. The trace is a JSON file that contains a timestamped stream of browser-internal events emitted by Chrome's rendering engine, compositor, and JavaScript runtime.

Chrome DevTools exposes the same tracing infrastructure when you record a Performance profile. The trace provides Measure Right with low-level visibility into exactly what work Chrome performed, including when:

  • Frames were committed.
  • Layout occurred.
  • Animation frames fired.

The trace covers the entire lifetime of the browser page, not only the benchmark interaction. To isolate the measured interaction, Measure Right brackets the benchmark run function with:

  • performance.mark("bench_start")
  • performance.mark("bench_end")

These emit blink.user_timing events into the trace that serve as precise anchors for the measurement window.

When Measure Right parses the trace, it first locates the start and end marks. It then discards all events outside that window. This prevents setup work in before, such as page navigation and waiting for elements, and post-run idle time from skewing the measured results. Within the scoped window, Measure Right looks for specific event types from Chrome’s rendering pipeline:

  • Commit events: Indicate when Chrome committed a frame to the compositor.
  • Layout events: Indicate browser reflow.
  • AnimationFrame async-start events: Indicate vsync ticks.

Measure Right derives the metrics below from the timestamps and durations of these events.

The following metrics are extracted from the Chrome trace for each iteration:

Metric Description
Duration (ms) Wall-clock time from bench_start to the end of the last Commit event, when Chrome commits the final rendered frame to the compositor. Measure Right subtracts idle vsync wait time from this value when rafLongDelay applies.
Average FPS Estimated frames per second, calculated from the cadence of AnimationFrame async-start events within the benchmark window.
Number of Commits Total count of Commit events across the benchmark run. Benchmarks perform multiple interactions, such as 16 scroll operations, so this value is expected to be greater than 1.
A high commit count relative to the number of interactions suggests that the grid triggers multiple rendering passes per interaction rather than efficiently batching updates.
Layouts Total count of Layout events. Unexpected layouts indicate that the grid triggers avoidable browser reflow work.
Max Delta Between Commits (ms) Spread between the first and last Commit timestamps. A large value indicates that rendering was spread across many passes over a long period rather than being batched efficiently.
RAF Long Delay (ms) Idle vsync wait time beyond the standard 16ms frame budget after bench_end. Measure Right subtracts this excess time from Duration to prevent idle browser wait time from inflating the result.
Memory (MB) JavaScript heap size captured via CDP after the interaction completes.

Full credit for the inspiration of this approach is given to the JS Framework Benchmark repository.

Iteration and Run Order

By default, benchmarks are run in round-robin order: each iteration cycles through all grids before starting the next iteration. This prevents any single grid from being consistently measured while the CPU or GPU is in a degraded thermal state, producing fairer comparisons. An alternative sequential mode (all iterations of one benchmark before moving to the next) is also available.

A 1-second pause is inserted between each run to allow the browser and system to settle.

Statistical Aggregation

After all iterations are complete, the following statistics are computed for each metric across all iterations:

Statistic Description
Mean Average value across all iterations.
Mode Most frequently occurring value after bucketing rounding.
Standard deviation Spread of values across iterations.
±1 standard deviation range Interval from mean - stdDev to mean + stdDev. This range helps indicate measurement stability.

Each metric is also normalized relative to the best-performing grid in the run, expressed as a relative distance percentage.

A relative distance of 0% means the grid achieved the best result for that metric.

Lower values are better for:

  • Duration
  • Memory
  • Commits
  • Layouts
  • Max delta between commits

Higher values are better for:

  • Average FPS

For more rigorous analysis of the raw CSV data, it is recommended to trim the top and bottom 5% of iteration values before computing the mean. This removes a small number of iterations affected by OS scheduling spikes, transient GC pauses, or CPU thermal events while preserving the bulk of the dataset.

This produces a 5% trimmed mean using values between the 5th and 95th percentiles. This technique is also used by js-framework-benchmark and other established JavaScript performance suites.

Individual Run Results

The summary statistics above are intended as a high-level overview. The run function also returns the raw measurements for every individual iteration, giving you the full dataset for each benchmark. This allows you to perform your own statistical analysis, for example, computing percentiles, trimming outliers, or applying alternative aggregation strategies.

The individual results are written to a CSV file (e.g. 01_scroll-10k.csv) alongside the summary, so they can be loaded into any analysis tool of your choice.

Fairness and Grid Normalization

All benchmarked grids use React 19:

  • react: ^19.2.5
  • react-dom: ^19.2.5

React 19 ships a revised concurrent rendering scheduler and altered reconciliation behavior compared to React 18, so results may differ from results produced with earlier React versions.

The source wraps the app in StrictMode. However, StrictMode double-render checks run only in development builds, so they do not affect production benchmark measurements.

Libraries Tested

The following libraries and versions are included in the benchmarks:

Grid Package(s) Version
LyteNyte Grid @1771technologies/lytenyte-core ^2.1.1
AG Grid ag-grid-community, ag-grid-react ^35.3.0
MUI X Data Grid @mui/x-data-grid-premium ^9.3.0
DevExtreme devextreme, devextreme-react ^25.2.7
Handsontable handsontable, @handsontable/react-wrapper ^17.1.0
Material React Table material-react-table ^3.2.1

Shared Data

All datasets are generated with the same seeded linear congruential generator:

  • Seed: 12345
  • Row value range: 0 -10

This ensures the generated values are identical and deterministic across runs. All data is precomputed before the tests run so that no benchmark iteration spends time generating data.

Benchmark Tests

The row counts and column counts vary by test group to target specific performance characteristics:

Test Benchmark Script Rows Columns Handsontable Rows Handsontable Columns
Scroll 10K 01-scroll-10k.ts 10,000 300 10,000 300
Scroll 200K 02-scroll-200k.ts 200,000 300 150,000 300
Scroll 500K 03-scroll-500k.ts 500,000 300 500,000 300
Scroll 1M 04-scroll-1000k.ts 1,000,000 300 1,000,000 300
Pinned 05-pinned.ts 200,000 300 20,000 300
Horizontal 06-horizontal.ts 50,000 300 20,000 300
Cell Updates 07-cell-updates.ts 1,000 × 50 datasets 100 1,000 × 50 datasets 300
Sorting 10K 08-sorting-10k.ts 10,000 300 10,000 300
Sorting 50K 09-sorting-50k.ts 50,000 300 50,000 300
Sorting 100K 10-sorting-100k.ts 100,000 300 100,000 300
Filtering 10K 11-filtering-10k.ts 10,000 300 10,000 300
Filtering 50K 12-filtering-50k.ts 50,000 300 50,000 300
Filtering 100K 13-filtering-100k.ts 100,000 300 100,000 300

For all scroll, sort, and filter tests, every column renders the same custom cell.

The custom cell is a div that fills the cell area and applies one of 10 background colors based on the row’s integer value. All grids use the same cell renderer so that per-cell rendering work is consistent across implementations.

For the cell updates test, the benchmark measures how quickly each grid replaces the entire dataset and renders the new rows. The test cycles through 50 prebuilt datasets in sequence.

Handsontable Data Differences

Handsontable has two structural differences from the other grids worth noting.

First, Handsontable’s native data model is a 2D array: Array<Array<number>>. The other grids use an array of objects with named keys: Array<Record<string, number>>.

All Handsontable tests use this format, which is the correct idiomatic input for the library and avoids any overhead from an incompatible data shape.

Second, several Handsontable tests use reduced row counts:

Test Other Grids Handsontable
Scroll 200K 200,000 rows 150,000 rows
Pinned 200,000 rows 20,000 rows
Horizontal 50,000 rows 20,000 rows

These reductions were necessary because Handsontable crashed with out-of-memory errors when loaded with the full row counts used by the other grids.

Where Handsontable's row count differs from the other grids, this is noted in the benchmark results so readers can account for it when interpreting comparisons.

Shared Grid Container and Viewport

All grids are rendered inside a shared GridContainer component that fixes the container to exactly 1920×1080 pixels. The Playwright browser viewport is set to 2000×1200 for all benchmark runs, so the grid always fills a consistent visible area regardless of which grid is under test.

Row Height and Cell Styling

All grids are configured to use a row height of 20px and a header height of 20px. Default cell padding is stripped to zero across all grids, either through each grid's API or via targeted CSS overrides, so that cell content sizing is as consistent as possible between implementations.

Enabled and Disabled Features

To ensure like-for-like rendering comparisons, the following normalizations are applied across all grids:

Normalization Applied Setting Rationale
Virtualization Enabled row and column virtualization on every grid that supports it. Virtualization is the standard production configuration for large datasets.
Row and column overscan Sets overscan or buffer values to 0, or the closest equivalent. This prevents grids with larger default buffers from rendering extra rows or columns beyond the visible area.
Animations Disabled row and cell animations where the grid provides an option, such as animateRows={false} in AG Grid. Animations add rendering work that is unrelated to the benchmarked interaction.
Pagination and toolbars Disabled pagination and toolbars on all grids. Only the grid body and header are rendered.
Column resizing and reordering Disabled resizing and reordering where applicable. This avoids extra event listeners or overhead unrelated to rendering.

Grid-Specific Configuration Policy

Where a grid's defaults would cause it to render materially more or less work than others, we override them to bring behavior in line with the rest of the field.

CSS overrides are used sparingly and only to normalize padding and row height. They are not used to hide or remove any part of a grid's rendering output. No internal APIs, private flags, or undocumented configuration options are used for any grid.

Running Runtime Benchmarks

Runtime Prerequisites

The benchmark scripts connect to the production build of the app running locally.

Do not use the Vite dev server for benchmark runs. React’s development mode adds overhead that can inflate timings and make cross-grid comparisons unreliable, such as:

  • StrictMode double-render checks.
  • Runtime prop-type checks.
  • Extra reconciliation paths.

The production build produces minified and optimized output that more closely reflects real-world deployment conditions.

Before running any script, make sure the preview server is up:

pnpm run build
pnpm run preview

The server listens on port 4173. Each benchmark script navigates to http://localhost:4173/pages/... so the server must remain running for the entire duration of the test.

Running a Single Benchmark

Each script in the bench folder is a self-contained test that can be run independently with tsx:

pnpm tsx ./bench/01-scroll-10k.ts
pnpm tsx ./bench/08-sorting-10k.ts

The script launches a Chrome browser via Playwright, runs the benchmark, and prints a summary table to the console when it finishes. It also writes a CSV file to the project root (e.g. 01_scroll-10k.csv) containing the raw measurements for every individual iteration.

Running All Benchmarks

There is no single command to run all benchmarks in sequence. Run each script individually:

pnpm tsx ./bench/01-scroll-10k.ts
pnpm tsx ./bench/02-scroll-200k.ts
pnpm tsx ./bench/03-scroll-500k.ts
pnpm tsx ./bench/04-scroll-1000k.ts
pnpm tsx ./bench/05-pinned.ts
pnpm tsx ./bench/06-horizontal.ts
pnpm tsx ./bench/07-cell-updates.ts
pnpm tsx ./bench/08-sorting-10k.ts
pnpm tsx ./bench/09-sorting-50k.ts
pnpm tsx ./bench/10-sorting-100k.ts
pnpm tsx ./bench/11-filtering-10k.ts
pnpm tsx ./bench/12-filtering-50k.ts
pnpm tsx ./bench/13-filtering-100k.ts

Running the full suite takes approximately 8 hours. For the most stable results, close all non-essential applications before starting and allow the machine to run uninterrupted.

Understanding the Output

Terminal Summary

When a script completes, it prints a console.table summary to the terminal. The table shows the mean value for each metric across all iterations for every grid included in that script.

CSV Results

The benchmark writes the full per-iteration dataset to a CSV file in the project root. The CSV includes every raw measurement, with one row per iteration per grid.

You can load the CSV into any spreadsheet or analysis tool for further processing. For details about the raw data fields, see the Individual Run Results section of the Benchmark Methodology.

Chrome Trace Files

Chrome trace files for the most recent CPU/timing pass of each benchmark are saved to a traces/ directory in the project root. Trace files are named by benchmark, for example:

traces/LyteNyte Scroll 10K.json`

These are overwritten on each run and can be loaded into chrome://tracing or the Chrome DevTools Performance panel for manual inspection.

Running a Subset of Grids

Each benchmark script defines which grids to include in the benchmarks array passed to run(). To run only specific grids, open the script and remove the entries you do not need from that array.

For example, to run only LyteNyte and AG Grid in the scroll 10K test, edit bench/01-scroll-10k.ts so the run() call reads:

const [result, items] = await run({
  benchmarks: [lng, ag],
  ...
});

Note

Some scripts already exclude certain grids. For example, Handsontable is omitted from the Scroll 200K script because it consistently crashes at that row count.

Changing the Iteration Count

The iteration count is set via the iterations parameter in the run() call at the bottom of each script. To run fewer iterations for a quick spot-check, change the value directly in the script:

const [result, items] = await run({
  benchmarks: [...],
  iterations: 5, // default is 50 for full runs
  ...
});

Keep in mind that the summary statistics (mean, standard deviation, range) become less reliable with fewer iterations, and outliers caused by background OS activity will have a greater impact on the results.

Running Bundle Size Measurement

Bundle sizes are measured independently of the runtime benchmarks and do not require the preview server to be running.

Run the check with:

pnpm tsx ./check-bundle-size.ts

This prints a summary table to the console and writes the produced bundle files to the dist/ directory.

How It Works

The bundle size script uses esbuild to bundle each grid's entry point. For each grid, the script:

  • Enables full minification with minify: true.
  • Targets ESM output.
  • Compresses the generated bundle with Node.js zlib using Z_BEST_COMPRESSION.
  • Reports the final gzipped file size.

The script treats react and react-dom as external dependencies and excludes them from all bundles, since every application already provides them.

Minimum and Maximum Bundles

Each grid is measured twice using two different entry points from the bundle-checks folder:

  • Minimum bundle (*-min.ts): Imports only the primary component needed to render the grid (e.g. import { Grid } from "@1771technologies/lytenyte-core"). This represents the smallest realistic footprint for an application that uses the grid with a single named import and benefits from tree shaking.

  • Maximum bundle (*-max.ts): Re-exports the entire package with export * from "...". This represents the upper bound, the full size of the library if nothing is tree-shaken away, as would occur in environments where tree shaking is unavailable or the application imports broadly.

Together these two values give a meaningful size range rather than a single potentially misleading figure.

Peer Dependencies

Some grids declare peer dependencies that are expected to be provided by the host application. For these grids the bundle is measured in two ways: once with those peer dependencies bundled in (the default), and once with them treated as external so only the grid's own code is counted. The grids with excluded peer dependencies are:

  • MUI X DataGrid: @mui/material, @emotion/react, @emotion/styled
  • Material React Table: @mui/material, @mui/x-date-pickers, @mui/icons-material, @emotion/react, @emotion/styled

LyteNyte Grid, AG Grid, DevExtreme, and Handsontable have no peer dependencies excluded, their reported sizes include all of their dependencies.

CSS and Compression

Bundle size measurement covers JavaScript only. CSS is not included in these measurements. Compression is gzip only (Z_BEST_COMPRESSION); Brotli is not measured. Results therefore represent what a gzip-capable CDN or web server would serve for the JS portion of each grid.

Output Files and CSV Schema

Output Locations

Running a benchmark script produces files in two locations:

  • CSV files: written to the project root, named after the benchmark script (e.g. 01_scroll-10k.csv, 08_sorting-10k.csv). One file per script run, overwritten on each run.
  • Chrome trace files: written to traces/ in the project root, named after each benchmark (e.g. traces/LyteNyte Scroll 10K.json). Overwritten on each run. These are standard Chrome trace files and can be loaded into chrome://tracing or the Chrome DevTools Performance panel.

Running the bundle size check writes minified JS bundles and their gzipped counterparts to dist/.

CSV Schema

Each CSV file has one header row followed by one data row per iteration per grid. If a script runs 2 grids for 50 iterations, the file will have 101 rows (1 header + 100 data rows).

The columns are:

Column Type Unit Description
name string Benchmark name, e.g. "LyteNyte Scroll 10K".
duration number ms Wall-clock time from bench_start to end of last Commit, minus any rafLongDelay correction.
avgFps number fps Average frames per second estimated from AnimationFrame event cadence.
memory number MB JS heap used size (JSHeapUsedSize) captured via CDP after the run.
numberCommits number count Total Commit events within the benchmark window.
layouts number count Total Layout events within the benchmark window.
maxDeltaBetweenCommits number ms Spread between first and last Commit timestamps.
rafLongDelay number ms Excess vsync wait beyond 16ms subtracted from duration; 0 when no correction was applied.

Interpreting Local Results

Prefer Aggregated Results Over a Single Run

A single benchmark iteration is not reliable on its own. Background OS activity, garbage collection timing, thermal throttling, and vsync jitter can make a single run unusually fast or slow.

Always draw conclusions from the aggregated statistics across all 50 iterations, in particular the mean and the standard deviation range.

If the standard deviation range is wide, the results are noisy. Re-run the benchmark with other processes closed. A single outlier in the CSV is not a concern; however, a consistently widespread deviation is.

When comparing two grids on a single metric, check whether their standard deviation ranges overlap. If the ranges overlap, the difference may not be statistically meaningful, even when the means differ.

Compare Within the Same Machine Only

Absolute values, such as duration in milliseconds, memory in megabytes, and FPS, are not portable across machines.

For example, 120ms on an M3 MacBook Pro and 120ms on a Windows desktop do not indicate identical performance. GPU acceleration, CPU thermal headroom, and browser scheduling can vary across machines.

Only compare results produced on the same hardware in the same session. When you share results publicly, include the hardware specifications.

Treat Memory Separately from Timing

Memory and timing metrics are captured in separate browser runs and measure different dimensions of grid performance:

  • Memory: JavaScript heap used size.
  • Timing: Duration, FPS, commits, and layouts.

A grid can be fast but memory-heavy. A grid can also use less memory but take longer to render.

Do not combine memory and timing metrics into a single score. Evaluate each dimension independently and decide which trade-off matters more for your use case.

Variations from Published Results

Published benchmark results are produced on specific hardware under controlled conditions (no background processes, GPU acceleration enabled, macOS on M-series silicon). Your local results may differ for several reasons:

  • Different Hardware: CPU and GPU performance, memory bandwidth, and thermal characteristics all affect rendering speed.
  • Background Processes: Any application consuming CPU or GPU during the run will inflate timing results.
  • Browser Version: Chrome's rendering engine changes between versions. Results are tied to the Chrome version installed at the time of measurement.
  • Thermal State: Sustained benchmark runs cause CPUs to throttle. Results recorded late in a long session may be slower than those recorded at the start. The round-robin iteration order mitigates this within a single script, but across a full 8-hour suite run, earlier tests will generally have more thermal headroom.
  • OS Scheduling: Windows and macOS schedule processes differently, and results are not expected to be numerically equivalent across operating systems.

Smoothness Proxy Limitations

The metrics captured here, duration, FPS, commit count, layout count, are objective measurements of browser rendering work, but they are not a perfect proxy for how smooth a grid feels to a user.

FPS as computed from AnimationFrame cadence measures how consistently the browser is scheduling animation callbacks, but does not capture frame drops that occur within a single vsync interval. A grid that schedules frames at a perfectly even cadence but does heavy work on each frame may feel jankier than its FPS figure suggests.

Duration measures how long the browser takes to process the benchmark interaction end-to-end, but a long duration is not always perceptible if most of the time is spent in off-screen work. Conversely, a grid that produces visual artifacts or repaints visible cells unnecessarily may feel worse than its duration figure suggests.

These metrics are best understood as engineering diagnostics, useful for identifying which grids do more or less work, rather than as direct predictions of user experience. For a complete picture, combine these results with manual inspection of the rendered output and your own user testing on representative hardware for your own use cases.

Known Limitations

Benchmark Scope

The benchmarks measure a specific set of rendering-intensive interactions using pregenerated numeric data and a custom color cell renderer.

A grid that performs well in these benchmarks may not be the best choice for every application. Grids also differ in areas that these benchmarks do not measure, including:

  • Feature set
  • API ergonomics
  • Accessibility support
  • Ecosystem maturity
  • Licensing

Use the benchmark results as one input alongside your own evaluation of the features and workloads your application requires.

Equivalent API Exposure

Some normalizations require workarounds for differences in how grids expose configuration. Row buffer sizes, overscan counts, and animation flags are named and scoped differently across libraries, and in some cases the closest available option is not an exact equivalent.

Where a perfect match is not possible, we select the option that yields the most comparable rendering behavior and document the choice in the Fairness and Grid Normalization section.

Measurement Window Assertions

All benchmark scripts include Playwright assertions inside the run function, so the assertions fall within the bench_start / bench_end measurement window.

The assertions serve two purposes:

  • Verify that the operation produced the expected result.
  • Synchronize the benchmark steps by waiting until the grid has finished rendering.

Each test type uses an assertion that matches the interaction being measured:

Test Type Assertion
Scroll test Use toBeInViewport() to confirm that the expected row has rendered into view.
Cell update tests Use toBeVisible() to wait until all dataset replacements have rendered.
Sort and filter tests Assert specific cell values to confirm that the operation completed.

The same assertions run for every grid, so the assertion overhead is consistent and does not favor any implementation.

Features Not Covered

These benchmarks measure rendering performance for scrolling, sorting, filtering, and dataset replacement only. The following capabilities are deliberately out of scope and performance on them is not reflected in these results:

  • Cell editing and inline form controls.
  • Row grouping, tree data, and aggregation.
  • Column resizing and reordering.
  • Keyboard navigation and focus management.
  • Accessibility (ARIA, screen reader compatibility).
  • Server-side data fetching and infinite scroll.
  • Server-side rendering (SSR) and hydration.
  • Frozen / sticky rows beyond the pinned test.
  • Theming and custom CSS rendering costs.
  • Plugin or extension overhead.

Browser and Machine Variance

Results are specific to the Chrome version, operating system, and hardware used during the run. Different browsers (Firefox, Safari) use different rendering pipelines and are not measured here.

Results on different machines, including cloud or CI environments, will not be numerically comparable to locally produced results and should not be treated as equivalent, even if the hardware specifications appear similar.

Feedback and Issues

Reporting Unfair Tests

If you believe a grid is configured in a way that produces misleading results, either because it is disadvantaged relative to its typical production usage or because it has been given an unrealistic advantage, please open a GitHub issue with:

  • The benchmark script and grid in question.
  • A description of what you believe is unfair.
  • A suggested alternative configuration and the reasoning behind it.

We will review all reports and update the benchmark if the concern is valid. Our goal is accurate measurement, not favorable results for any particular grid.

Requesting Additional Grids

If you would like to see a grid added to the benchmarks, please open a GitHub issue with the grid's name, package, and a link to its documentation. We will consider adding it if we can configure it fairly and consistently with the existing grids. Note that grids require a permissive enough license to be used in a public benchmark without restriction.

Requesting Additional Scenarios

If you would like to see a new benchmark scenario added, such as a different interaction type, a different row or column count, or a specific rendering pattern, please open a GitHub issue describing the use case and why it would be a meaningful addition. Scenarios that reflect real-world production workloads and are reproducible across all included grids are most likely to be accepted.

Legal Disclaimer

This README and benchmark project are provided for informational purposes only. The benchmark methodology, scripts, and configuration are not an endorsement, recommendation, or guarantee of performance for any grid library. Results may vary based on hardware, browser version, operating system, system load, grid configuration, dataset shape, and application requirements.

All third-party names, packages, trademarks, and products referenced in this project belong to their respective owners. Their inclusion is for comparative benchmarking only and does not imply sponsorship, endorsement, affiliation, or approval.

We aim to configure each grid fairly and consistently. If you maintain one of the included grids and believe a benchmark configuration is incorrect or unrepresentative, please open an issue or pull request with details.

This project is provided “as is,” without warranties of any kind. Users should review the methodology, inspect the source code, and validate results in their own environment before making technical or business decisions.

About

A set of benchmark tests designed to compare the select performance characteristics of popular React data grid libraries.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages