Virtualization benchmark
The lab covers list, flow-grid, masonry, track-grid, and spatial layouts through equivalent public APIs. Within each family, adapters receive the same deterministic 100,000 items and the suite measures initial display, scroll response, and settling after inserts, moves, removals, and size changes.
List
Flow grid
This condition is unsupported or has no measurement.
View raw measurements
Initial render and scrolling
| Library | Input mode | Setup | First items | Stable layout | Scroll median | Scroll p95 | Samples |
|---|---|---|---|---|---|---|---|
| Sectile Virtual | fixed | 19.2 ms | 19.2 ms | 19.5 ms | 3 ms | 4 ms | 100 |
| Sectile Virtual | estimated | 14.6 ms | 14.6 ms | 14.8 ms | 2.8 ms | 3.6 ms | 100 |
| Sectile Virtual | automatic | 16.3 ms | 16.3 ms | 16.5 ms | 1.9 ms | 2.4 ms | 100 |
| React Virtuoso | automatic | 0.9 ms | 11.9 ms | 12 ms | 0.5 ms | 0.7 ms | 100 |
Layout mutations
| Library | Input mode | Operation | Location | Scroll median | p95 | Samples | Failed |
|---|---|---|---|---|---|---|---|
| Sectile Virtual | fixed | insert | start | 8.1 ms | 9.1 ms | 30 | 0 |
| Sectile Virtual | fixed | insert | middle | 8.1 ms | 9.2 ms | 30 | 0 |
| Sectile Virtual | fixed | insert | end | 8.1 ms | 9.1 ms | 30 | 0 |
| Sectile Virtual | fixed | move | start | 8 ms | 9.1 ms | 30 | 0 |
| Sectile Virtual | fixed | move | middle | 8.1 ms | 9.2 ms | 30 | 0 |
| Sectile Virtual | fixed | move | end | 8 ms | 9 ms | 30 | 0 |
| Sectile Virtual | fixed | remove | start | 8.1 ms | 9 ms | 30 | 0 |
| Sectile Virtual | fixed | remove | middle | 8.1 ms | 9.2 ms | 50 | 0 |
| Sectile Virtual | fixed | remove | end | 8.1 ms | 9.1 ms | 30 | 0 |
| Sectile Virtual | estimated | insert | start | 8.2 ms | 9.3 ms | 30 | 0 |
| Sectile Virtual | automatic | insert | start | 8.2 ms | 9.1 ms | 30 | 0 |
| React Virtuoso | automatic | insert | start | 8.2 ms | 9.2 ms | 30 | 0 |
| Sectile Virtual | estimated | insert | middle | 7.9 ms | 8.3 ms | 30 | 0 |
| Sectile Virtual | automatic | insert | middle | 8 ms | 8.9 ms | 30 | 0 |
| React Virtuoso | automatic | insert | middle | 8.2 ms | 8.8 ms | 30 | 0 |
| Sectile Virtual | estimated | insert | end | 8.1 ms | 9.1 ms | 30 | 0 |
| Sectile Virtual | automatic | insert | end | 8.1 ms | 8.5 ms | 30 | 0 |
| React Virtuoso | automatic | insert | end | 8.2 ms | 9.1 ms | 30 | 0 |
| Sectile Virtual | estimated | move | start | 8.1 ms | 9 ms | 30 | 0 |
| Sectile Virtual | automatic | move | start | 8.2 ms | 8.3 ms | 30 | 0 |
| React Virtuoso | automatic | move | start | 8.1 ms | 9.1 ms | 30 | 0 |
| Sectile Virtual | estimated | move | middle | 8 ms | 8.9 ms | 30 | 0 |
| Sectile Virtual | automatic | move | middle | 8.1 ms | 9 ms | 30 | 0 |
| React Virtuoso | automatic | move | middle | 8.2 ms | 9.1 ms | 30 | 0 |
| Sectile Virtual | estimated | move | end | 8.1 ms | 9 ms | 30 | 0 |
| Sectile Virtual | automatic | move | end | 8.1 ms | 9 ms | 30 | 0 |
| React Virtuoso | automatic | move | end | 8.1 ms | 9.2 ms | 30 | 0 |
| Sectile Virtual | estimated | remove | start | 8.2 ms | 8.4 ms | 30 | 0 |
| Sectile Virtual | automatic | remove | start | 8.2 ms | 8.3 ms | 30 | 0 |
| React Virtuoso | automatic | remove | start | 8.2 ms | 9.2 ms | 30 | 0 |
| Sectile Virtual | estimated | remove | middle | 8.1 ms | 8.8 ms | 30 | 0 |
| Sectile Virtual | automatic | remove | middle | 8 ms | 8.4 ms | 30 | 0 |
| React Virtuoso | automatic | remove | middle | 8.2 ms | 9.2 ms | 30 | 0 |
| Sectile Virtual | estimated | remove | end | 8.1 ms | 8.9 ms | 30 | 0 |
| Sectile Virtual | automatic | remove | end | 8.1 ms | 8.3 ms | 30 | 0 |
| React Virtuoso | automatic | remove | end | 8.2 ms | 8.9 ms | 30 | 0 |
| Sectile Virtual | estimated | resize | start | 8 ms | 8.8 ms | 30 | 0 |
| Sectile Virtual | automatic | resize | start | 7.7 ms | 9.3 ms | 30 | 0 |
| React Virtuoso | automatic | resize | start | 8.2 ms | 9.3 ms | 30 | 0 |
| Sectile Virtual | estimated | resize | middle | 8.1 ms | 9.2 ms | 30 | 0 |
| Sectile Virtual | automatic | resize | middle | 8.1 ms | 8.6 ms | 30 | 0 |
| React Virtuoso | automatic | resize | middle | 8.2 ms | 9.2 ms | 30 | 0 |
| Sectile Virtual | estimated | resize | end | 8.1 ms | 8.3 ms | 30 | 0 |
| Sectile Virtual | automatic | resize | end | 8.2 ms | 8.3 ms | 30 | 0 |
| React Virtuoso | automatic | resize | end | 8.1 ms | 8.7 ms | 30 | 0 |
Masonry
This condition is unsupported or has no measurement.
View raw measurements
Initial render and scrolling
| Library | Input mode | Setup | First items | Stable layout | Scroll median | Scroll p95 | Samples |
|---|---|---|---|---|---|---|---|
| Sectile Virtual | fixed | 15.2 ms | 15.2 ms | 15.4 ms | 2.8 ms | 3.5 ms | 100 |
| Sectile Virtual | estimated | 9.6 ms | 9.6 ms | 9.9 ms | 6.4 ms | 8.9 ms | 100 |
| Sectile Virtual | automatic | 15.8 ms | 15.8 ms | 16.1 ms | 6.1 ms | 21 ms | 100 |
| TanStack Virtual | estimated | 18 ms | 18 ms | 18.2 ms | 0.4 ms | 0.6 ms | 100 |
Layout mutations
| Library | Input mode | Operation | Location | Scroll median | p95 | Samples | Failed |
|---|---|---|---|---|---|---|---|
| Sectile Virtual | fixed | insert | start | 8.1 ms | 8.3 ms | 30 | 0 |
| Sectile Virtual | fixed | insert | middle | 8.2 ms | 9.2 ms | 30 | 0 |
| Sectile Virtual | fixed | insert | end | 8.1 ms | 9 ms | 30 | 0 |
| Sectile Virtual | fixed | move | start | 8.1 ms | 8.9 ms | 30 | 0 |
| Sectile Virtual | fixed | move | middle | 8.1 ms | 9.1 ms | 30 | 0 |
| Sectile Virtual | fixed | move | end | 8.1 ms | 9.1 ms | 30 | 0 |
| Sectile Virtual | fixed | remove | start | 8.2 ms | 9.2 ms | 30 | 0 |
| Sectile Virtual | fixed | remove | middle | 8.1 ms | 9.2 ms | 30 | 0 |
| Sectile Virtual | fixed | remove | end | 8.1 ms | 8.2 ms | 30 | 0 |
| Sectile Virtual | estimated | insert | start | 81.4 ms | 134 ms | 30 | 0 |
| Sectile Virtual | automatic | insert | start | 68.4 ms | 138.9 ms | 30 | 0 |
| TanStack Virtual | estimated | insert | start | 8.3 ms | 9.6 ms | 30 | 0 |
| Sectile Virtual | estimated | insert | middle | 43.2 ms | 74.8 ms | 40 | 0 |
| Sectile Virtual | automatic | insert | middle | 51.1 ms | 85.3 ms | 30 | 0 |
| TanStack Virtual | estimated | insert | middle | 8.6 ms | 11.1 ms | 30 | 0 |
| Sectile Virtual | estimated | insert | end | 7 ms | 7.8 ms | 30 | 0 |
| Sectile Virtual | automatic | insert | end | 6.8 ms | 7.3 ms | 30 | 0 |
| TanStack Virtual | estimated | insert | end | 8.2 ms | 9.7 ms | 30 | 0 |
| Sectile Virtual | estimated | move | start | 68.3 ms | 136.1 ms | 30 | 0 |
| Sectile Virtual | automatic | move | start | 67.8 ms | 136.2 ms | 30 | 0 |
| TanStack Virtual | estimated | move | start | 8.3 ms | 9.8 ms | 30 | 0 |
| Sectile Virtual | estimated | move | middle | 37.8 ms | 72.7 ms | 30 | 0 |
| Sectile Virtual | automatic | move | middle | 38.9 ms | 72.7 ms | 30 | 0 |
| TanStack Virtual | estimated | move | middle | 8.2 ms | 10.3 ms | 30 | 0 |
| Sectile Virtual | estimated | move | end | 6.9 ms | 8.6 ms | 30 | 0 |
| Sectile Virtual | automatic | move | end | 6.8 ms | 7.2 ms | 30 | 0 |
| TanStack Virtual | estimated | move | end | 8.3 ms | 9.3 ms | 30 | 0 |
| Sectile Virtual | estimated | remove | start | 73.6 ms | 136.7 ms | 30 | 0 |
| Sectile Virtual | automatic | remove | start | 71.9 ms | 141.3 ms | 30 | 0 |
| TanStack Virtual | estimated | remove | start | 8.3 ms | 10.2 ms | 30 | 0 |
| Sectile Virtual | estimated | remove | middle | 40.4 ms | 76.5 ms | 30 | 0 |
| Sectile Virtual | automatic | remove | middle | 40.1 ms | 71.6 ms | 30 | 0 |
| TanStack Virtual | estimated | remove | middle | 8.3 ms | 9.3 ms | 30 | 0 |
| Sectile Virtual | estimated | remove | end | 6.4 ms | 6.8 ms | 30 | 0 |
| Sectile Virtual | automatic | remove | end | 6.6 ms | 7.2 ms | 30 | 0 |
| TanStack Virtual | estimated | remove | end | 8.3 ms | 9.3 ms | 30 | 0 |
| Sectile Virtual | estimated | resize | start | 3.1 ms | 4.4 ms | 40 | 0 |
| Sectile Virtual | automatic | resize | start | 3.2 ms | 3.7 ms | 30 | 0 |
| TanStack Virtual | estimated | resize | start | 8.3 ms | 9.6 ms | 30 | 0 |
| Sectile Virtual | estimated | resize | middle | 3.1 ms | 3.4 ms | 30 | 0 |
| Sectile Virtual | automatic | resize | middle | 3 ms | 5.9 ms | 30 | 0 |
| TanStack Virtual | estimated | resize | middle | 8.2 ms | 9.3 ms | 30 | 0 |
| Sectile Virtual | estimated | resize | end | 3.2 ms | 4.3 ms | 30 | 0 |
| Sectile Virtual | automatic | resize | end | 3.1 ms | 4.2 ms | 30 | 0 |
| TanStack Virtual | estimated | resize | end | 8.2 ms | 10.2 ms | 30 | 0 |
Track grid
This condition is unsupported or has no measurement.
View raw measurements
Initial render and scrolling
| Library | Input mode | Setup | First items | Stable layout | Scroll median | Scroll p95 | Samples |
|---|---|---|---|---|---|---|---|
| Sectile Virtual | fixed | 12.6 ms | 12.6 ms | 13.2 ms | 8.7 ms | 13.5 ms | 100 |
| react-window | fixed | 2.7 ms | 2.7 ms | 3.5 ms | 8 ms | 9.5 ms | 100 |
| Virtua | estimated | Initial-render failure · geometry:item-37215 | |||||
Layout mutations
| Library | Input mode | Operation | Location | Scroll median | p95 | Samples | Failed |
|---|---|---|---|---|---|---|---|
| Sectile Virtual | fixed | insert | start | 11.4 ms | 16.6 ms | 30 | 0 |
| react-window | fixed | insert | start | 7.9 ms | 8.3 ms | 30 | 0 |
| Sectile Virtual | fixed | insert | middle | 12.4 ms | 17.6 ms | 30 | 0 |
| react-window | fixed | insert | middle | 7.7 ms | 8 ms | 30 | 0 |
| Sectile Virtual | fixed | insert | end | 9.7 ms | 14.3 ms | 30 | 0 |
| react-window | fixed | insert | end | 7.5 ms | 8.4 ms | 30 | 0 |
| Sectile Virtual | fixed | move | start | 9.1 ms | 13.9 ms | 30 | 0 |
| react-window | fixed | move | start | 7.8 ms | 8.8 ms | 50 | 0 |
| Sectile Virtual | fixed | move | middle | 10.1 ms | 15.6 ms | 30 | 0 |
| react-window | fixed | move | middle | 7.5 ms | 7.9 ms | 30 | 0 |
| Sectile Virtual | fixed | move | end | 10.2 ms | 15.6 ms | 30 | 0 |
| react-window | fixed | move | end | 7.5 ms | 10.9 ms | 50 | 0 |
| Sectile Virtual | fixed | remove | start | 11.3 ms | 16.6 ms | 30 | 0 |
| react-window | fixed | remove | start | 7.9 ms | 8.2 ms | 30 | 0 |
| Sectile Virtual | fixed | remove | middle | 12.4 ms | 18.8 ms | 30 | 0 |
| react-window | fixed | remove | middle | 7.4 ms | 8.3 ms | 30 | 0 |
| Sectile Virtual | fixed | remove | end | 10.2 ms | 15.5 ms | 30 | 0 |
| react-window | fixed | remove | end | 7.5 ms | 8.7 ms | 30 | 0 |
| Sectile Virtual | fixed | resize | start | 9.6 ms | 14.6 ms | 30 | 0 |
| react-window | fixed | resize | start | 7.9 ms | 8.8 ms | 30 | 0 |
| Sectile Virtual | fixed | resize | middle | 10.7 ms | 16.1 ms | 30 | 0 |
| react-window | fixed | resize | middle | 7.5 ms | 8.3 ms | 30 | 0 |
| Sectile Virtual | fixed | resize | end | 9.5 ms | 14.5 ms | 30 | 0 |
| react-window | fixed | resize | end | 7.5 ms | 8.4 ms | 30 | 0 |
Spatial
View raw measurements
Initial render and scrolling
| Library | Input mode | Setup | First items | Stable layout | Scroll median | Scroll p95 | Samples |
|---|---|---|---|---|---|---|---|
| Sectile Virtual | positioned | 90 ms | 90 ms | 90.4 ms | 7.2 ms | 12 ms | 100 |
Layout mutations
| Library | Input mode | Operation | Location | Scroll median | p95 | Samples | Failed |
|---|---|---|---|---|---|---|---|
| Sectile Virtual | positioned | insert | start | 141 ms | 168.5 ms | 30 | 0 |
| Sectile Virtual | positioned | insert | middle | 100.9 ms | 130.8 ms | 30 | 0 |
| Sectile Virtual | positioned | insert | end | 8.2 ms | 9.2 ms | 30 | 0 |
| Sectile Virtual | positioned | move | start | 8.1 ms | 9.2 ms | 30 | 0 |
| Sectile Virtual | positioned | move | middle | 8 ms | 9 ms | 30 | 0 |
| Sectile Virtual | positioned | move | end | 8 ms | 9.5 ms | 30 | 0 |
| Sectile Virtual | positioned | remove | start | 139.5 ms | 161.7 ms | 30 | 0 |
| Sectile Virtual | positioned | remove | middle | 98.9 ms | 130.7 ms | 30 | 0 |
| Sectile Virtual | positioned | remove | end | 8 ms | 8.5 ms | 30 | 0 |
| Sectile Virtual | positioned | resize | start | 8.1 ms | 9 ms | 30 | 0 |
| Sectile Virtual | positioned | resize | middle | 8 ms | 9 ms | 30 | 0 |
| Sectile Virtual | positioned | resize | end | 8 ms | 8.6 ms | 30 | 0 |
Row profiles
The uniform profile isolates each library's base cost with identical 72px rows. The heterogeneous profile uses 256 deterministic combinations of titles, summaries, tags, and expanded details. Its measured row heights range from 71px to 159px.
The application does not calculate per-row heights. Each library measures the DOM it renders, while a separate calibration fixture supplies geometry only to the correctness validator. Visible rows are checked for order, overlap, gaps, viewport coverage, and anchor position. Total-height estimation error is reported separately for heterogeneous rows because unseen content has not been measured yet.
For two-dimensional families, exact modes require the full content extent and absolute item geometry. When a public API accepts exact per-item sizes but lazily derives their aggregate extent, absolute item geometry remains exact while the unseen total extent is provisional. Estimated and DOM-discovered modes instead require the requested data revision, finite provisional extents, correct rendered-item identity and size, and a non-empty viewport. Mutation completion requires the affected inserted, adjacent-moved, removed, or resized items to be observable in the rendered DOM. List and two-dimensional families share the same target-positioning and frame-settlement kernel; only their snapshot and oracle checks differ. Two-dimensional mutation samples retain both the first observer probe with correct geometry and the following verified-frame result. Probe cost is reported separately, while the existing frame result remains the conservative comparison score. This keeps unseen-item estimates and implementation-defined masonry lane choices out of the correctness score without accepting an outer framework commit as completed virtualizer work. Fixed flow-grid and masonry conditions use uniform items, and size-change scenarios run only where the public API supports changing item size.
Why height input is split into three modes
The suite measures exact heights, estimates, and omitted height input separately. Combining them would hide the cost difference between a fixed-size fast path and DOM measurement.
- Fixed: the application supplies the exact 72px height. This mode is available for the uniform profile.
- Estimated: the application supplies one common 72px estimate, then the library reads the actual DOM height.
- No height input: the application omits both an exact height and an estimate. Sectile renders an initial viewport-sized sample, measures those elements, and derives the remaining extent from those measurements.
Libraries that cannot start without a height or estimate are not given a synthetic value. The support table records the required input instead.
The height-change scenario expands one visible row to 96px. The browser DOM supplies the measured height; the exact 96px value is not passed back into the virtualization API.
Each adapter follows its library’s required integration. react-virtualized requires an explicit measurement-cache invalidation and layout recomputation call. The other adapters use APIs that observe DOM size changes automatically. That application-side work remains inside the timing boundary.
Collection mutation scenario
Insert, move, remove, and height-change operations run at three collection positions:
- start: the first row;
- middle: row 50,000 aligned with the viewport start;
- end: the final row visible at the bottom boundary.
Insert, remove, and height change affect one row at that position. Move swaps it with the adjacent row. Every sample starts from the same 100,000-row collection, and each scenario runs up to 50 times.
Timing starts with the update request and ends on the first frame where DOM order, row heights, total scroll height, and anchor position are all correct. Independent instances contribute batches of 5, 5, 10, 10, 10, and 10 samples; a stable median and p95 can stop a condition after 30 samples. Between samples, the runner restores and verifies the original collection; a failed restore discards that instance and mounts a fresh one. Clean and recovered samples are counted separately. A recovered sample retains both its recovery time and every observed correctness failure.
The settle bars cover every sample that reached a correct screen. Recovery bars use only samples that showed an incorrect screen before recovering. A failed condition is shown instead of a timing bar.
Failure criteria
Every frame is checked for missing or duplicate IDs, incorrect order or height, gaps, overlap, blank viewport regions, total scroll-height errors, and unexpected anchor movement. Any incorrect frame creates a correctness record. An incorrect layout that remains unchanged for at least 300ms and eight frames is a hard failure. A layout whose coordinates keep changing remains under observation for up to two seconds. Sectile failures carry fatal severity in the raw result.
Initial render and scrolling
Both row profiles render 100,000 rows in a 720 × 480px viewport. Text, CSS, input data, and the requested eight-row overscan target are shared. The no-height-input path starts from measured DOM rather than a library-wide numeric fallback.
Each condition first runs once in a fresh same-origin browsing context. That diagnostic records the first correct layout and the next browser presentation opportunity. It is excluded from the warm score. The main runner then completes an untimed warm-up mount and rotates library order across three to five measured rounds. Warm rendering is split into committed scroller output, first row output, and the first state with correct viewport geometry. A condition stops after three rounds when its cumulative median and p95 are stable; otherwise it continues through round five. Uniform rows also require the exact total scroll height. Heterogeneous rows retain total-height estimation error as a separate result.
Each round discards five warm-up scrolls and records the next 20. The harness changes scrollTop after a frame boundary. Timing starts when the browser begins delivering the native scroll event and ends immediately after the runner reads the resulting DOM geometry. The exact target row, contiguous row geometry, and viewport coverage are then validated against that snapshot outside the timed interval. Uniform rows also validate total scroll height.
The chart uses the conservative upper bound taken after geometry reads. Raw samples also retain the lower bound before those reads, probe cost, correctness-check count, round and sample number, MAD, and per-round ranges. This keeps validation work out of the score without hiding measurement uncertainty or a slow round.
The list-through-spatial observation was recorded on 2026-08-31 in Chrome 151 on Apple Silicon macOS from the clean production build of commit ce84a7b. Sectile completed every supported initial-render and mutation condition without a correctness failure. Absolute timings vary by machine and browser state; compare relative results together with correctness data.
The suite covers TanStack Virtual, react-window, React Virtuoso, react-virtualized, Virtua, and Vue Virtual Scroller. Runner code and committed JSON live in benchmarks/virtual-ecosystem.
pnpm --filter @sectile/benchmark-virtual-ecosystem devIsolated Sectile layout work
The browser suite includes framework and DOM work. This command isolates @sectile/virtual extent indexing, viewport queries, and layout-plan creation:
pnpm --filter @sectile/virtual benchmarkObserved on 2026-08-27 with Node 24.19.0, Apple Silicon, macOS:
| Operation | Median time |
|---|---|
| Extent lookup pair, 100k | 0.676 µs |
| Extent lookup pair, 1m | 0.870 µs |
| Linear window reduction, 100k | 1.237 µs |
| Linear materialized plan, 100k | 7.694 µs |
| Linear changed 32-item measurement batch, 100k | 2.857 µs |
| Sparse track-grid query, 100k regions | 13.841 µs |
| Sparse track-grid changed 32-row batch | 5.451 µs |
| Masonry query, 100k items / 8 lanes | 21.873 µs |
| Spatial packed-tree query, 100k overlapping items | 39.982 µs |
| Strategy | 100k index construction |
|---|---|
| sparse track grid | 45.742 ms |
| masonry, 8 lanes | 16.467 ms |
| spatial packed tree | 73.927 ms |
The query viewport moves across each domain instead of repeatedly hitting one cached coordinate. The raw report also includes partitioned track-grid construction, pinned-region queries, and changed-track batches. Re-run before and after a change on the same machine when evaluating a regression.
