Skip to content

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.

Open the benchmark lab →

List

Comparison conditions
Uniform heights
Warm layout readiness
Fixed height
Estimate provided
Height omitted
Uniform heights · Warm layout readiness · Fixed heightMax 14 ms
Warm layout readiness
Sectile Virtualv0.11.1 · Vue 3.5.223 rounds
10.0 ms
TanStack Virtualv3.14.10 · React 19.2.84 rounds
6.5 ms
react-windowv2.3.0 · React 19.2.84 rounds
1.9 ms
React Virtuosov4.18.12 · React 19.2.84 rounds
14.0 ms
react-virtualizedv9.22.6 · React 19.2.84 rounds
2.1 ms
Virtuav0.50.5 · React 19.2.83 rounds
6.7 ms
Vue Virtual Scrollerv3.0.5 · Vue 3.5.225 rounds
13.9 ms
Measurement criteria for this chart
Data
100,000 equal-height rows
Viewport
720 × 480px · 8-row overscan
Height input
Exact 72px supplied
Repetition
3–5 rounds with rotated library order
Completion
Correct total height and viewport geometry
Total height
Matches the measured total
Environment
Chrome 151 · Apple Silicon · macOS

Flow grid

Comparison conditions
Initial render
Fixed
Estimate provided
Automatic measurement
Initial render · FixedMax 20 ms
SetupFirst itemsStable layout
Sectile Virtualv0.11.1 · Vue 3.5.225 rounds
19.2 ms
19.2 ms
19.5 ms
React Virtuosov4.18.12 · React 19.2.8

This condition is unsupported or has no measurement.

View raw measurements

Initial render and scrolling

LibraryInput modeSetupFirst itemsStable layoutScroll medianScroll p95Samples
Sectile Virtualfixed19.2 ms19.2 ms19.5 ms3 ms4 ms100
Sectile Virtualestimated14.6 ms14.6 ms14.8 ms2.8 ms3.6 ms100
Sectile Virtualautomatic16.3 ms16.3 ms16.5 ms1.9 ms2.4 ms100
React Virtuosoautomatic0.9 ms11.9 ms12 ms0.5 ms0.7 ms100

Layout mutations

LibraryInput modeOperationLocationScroll medianp95SamplesFailed
Sectile Virtualfixedinsertstart8.1 ms9.1 ms300
Sectile Virtualfixedinsertmiddle8.1 ms9.2 ms300
Sectile Virtualfixedinsertend8.1 ms9.1 ms300
Sectile Virtualfixedmovestart8 ms9.1 ms300
Sectile Virtualfixedmovemiddle8.1 ms9.2 ms300
Sectile Virtualfixedmoveend8 ms9 ms300
Sectile Virtualfixedremovestart8.1 ms9 ms300
Sectile Virtualfixedremovemiddle8.1 ms9.2 ms500
Sectile Virtualfixedremoveend8.1 ms9.1 ms300
Sectile Virtualestimatedinsertstart8.2 ms9.3 ms300
Sectile Virtualautomaticinsertstart8.2 ms9.1 ms300
React Virtuosoautomaticinsertstart8.2 ms9.2 ms300
Sectile Virtualestimatedinsertmiddle7.9 ms8.3 ms300
Sectile Virtualautomaticinsertmiddle8 ms8.9 ms300
React Virtuosoautomaticinsertmiddle8.2 ms8.8 ms300
Sectile Virtualestimatedinsertend8.1 ms9.1 ms300
Sectile Virtualautomaticinsertend8.1 ms8.5 ms300
React Virtuosoautomaticinsertend8.2 ms9.1 ms300
Sectile Virtualestimatedmovestart8.1 ms9 ms300
Sectile Virtualautomaticmovestart8.2 ms8.3 ms300
React Virtuosoautomaticmovestart8.1 ms9.1 ms300
Sectile Virtualestimatedmovemiddle8 ms8.9 ms300
Sectile Virtualautomaticmovemiddle8.1 ms9 ms300
React Virtuosoautomaticmovemiddle8.2 ms9.1 ms300
Sectile Virtualestimatedmoveend8.1 ms9 ms300
Sectile Virtualautomaticmoveend8.1 ms9 ms300
React Virtuosoautomaticmoveend8.1 ms9.2 ms300
Sectile Virtualestimatedremovestart8.2 ms8.4 ms300
Sectile Virtualautomaticremovestart8.2 ms8.3 ms300
React Virtuosoautomaticremovestart8.2 ms9.2 ms300
Sectile Virtualestimatedremovemiddle8.1 ms8.8 ms300
Sectile Virtualautomaticremovemiddle8 ms8.4 ms300
React Virtuosoautomaticremovemiddle8.2 ms9.2 ms300
Sectile Virtualestimatedremoveend8.1 ms8.9 ms300
Sectile Virtualautomaticremoveend8.1 ms8.3 ms300
React Virtuosoautomaticremoveend8.2 ms8.9 ms300
Sectile Virtualestimatedresizestart8 ms8.8 ms300
Sectile Virtualautomaticresizestart7.7 ms9.3 ms300
React Virtuosoautomaticresizestart8.2 ms9.3 ms300
Sectile Virtualestimatedresizemiddle8.1 ms9.2 ms300
Sectile Virtualautomaticresizemiddle8.1 ms8.6 ms300
React Virtuosoautomaticresizemiddle8.2 ms9.2 ms300
Sectile Virtualestimatedresizeend8.1 ms8.3 ms300
Sectile Virtualautomaticresizeend8.2 ms8.3 ms300
React Virtuosoautomaticresizeend8.1 ms8.7 ms300

Masonry

Comparison conditions
Initial render
Fixed
Estimate provided
Automatic measurement
Initial render · FixedMax 15 ms
SetupFirst itemsStable layout
Sectile Virtualv0.11.1 · Vue 3.5.225 rounds
15.2 ms
15.2 ms
15.4 ms
TanStack Virtualv3.14.10 · React 19.2.8

This condition is unsupported or has no measurement.

View raw measurements

Initial render and scrolling

LibraryInput modeSetupFirst itemsStable layoutScroll medianScroll p95Samples
Sectile Virtualfixed15.2 ms15.2 ms15.4 ms2.8 ms3.5 ms100
Sectile Virtualestimated9.6 ms9.6 ms9.9 ms6.4 ms8.9 ms100
Sectile Virtualautomatic15.8 ms15.8 ms16.1 ms6.1 ms21 ms100
TanStack Virtualestimated18 ms18 ms18.2 ms0.4 ms0.6 ms100

Layout mutations

LibraryInput modeOperationLocationScroll medianp95SamplesFailed
Sectile Virtualfixedinsertstart8.1 ms8.3 ms300
Sectile Virtualfixedinsertmiddle8.2 ms9.2 ms300
Sectile Virtualfixedinsertend8.1 ms9 ms300
Sectile Virtualfixedmovestart8.1 ms8.9 ms300
Sectile Virtualfixedmovemiddle8.1 ms9.1 ms300
Sectile Virtualfixedmoveend8.1 ms9.1 ms300
Sectile Virtualfixedremovestart8.2 ms9.2 ms300
Sectile Virtualfixedremovemiddle8.1 ms9.2 ms300
Sectile Virtualfixedremoveend8.1 ms8.2 ms300
Sectile Virtualestimatedinsertstart81.4 ms134 ms300
Sectile Virtualautomaticinsertstart68.4 ms138.9 ms300
TanStack Virtualestimatedinsertstart8.3 ms9.6 ms300
Sectile Virtualestimatedinsertmiddle43.2 ms74.8 ms400
Sectile Virtualautomaticinsertmiddle51.1 ms85.3 ms300
TanStack Virtualestimatedinsertmiddle8.6 ms11.1 ms300
Sectile Virtualestimatedinsertend7 ms7.8 ms300
Sectile Virtualautomaticinsertend6.8 ms7.3 ms300
TanStack Virtualestimatedinsertend8.2 ms9.7 ms300
Sectile Virtualestimatedmovestart68.3 ms136.1 ms300
Sectile Virtualautomaticmovestart67.8 ms136.2 ms300
TanStack Virtualestimatedmovestart8.3 ms9.8 ms300
Sectile Virtualestimatedmovemiddle37.8 ms72.7 ms300
Sectile Virtualautomaticmovemiddle38.9 ms72.7 ms300
TanStack Virtualestimatedmovemiddle8.2 ms10.3 ms300
Sectile Virtualestimatedmoveend6.9 ms8.6 ms300
Sectile Virtualautomaticmoveend6.8 ms7.2 ms300
TanStack Virtualestimatedmoveend8.3 ms9.3 ms300
Sectile Virtualestimatedremovestart73.6 ms136.7 ms300
Sectile Virtualautomaticremovestart71.9 ms141.3 ms300
TanStack Virtualestimatedremovestart8.3 ms10.2 ms300
Sectile Virtualestimatedremovemiddle40.4 ms76.5 ms300
Sectile Virtualautomaticremovemiddle40.1 ms71.6 ms300
TanStack Virtualestimatedremovemiddle8.3 ms9.3 ms300
Sectile Virtualestimatedremoveend6.4 ms6.8 ms300
Sectile Virtualautomaticremoveend6.6 ms7.2 ms300
TanStack Virtualestimatedremoveend8.3 ms9.3 ms300
Sectile Virtualestimatedresizestart3.1 ms4.4 ms400
Sectile Virtualautomaticresizestart3.2 ms3.7 ms300
TanStack Virtualestimatedresizestart8.3 ms9.6 ms300
Sectile Virtualestimatedresizemiddle3.1 ms3.4 ms300
Sectile Virtualautomaticresizemiddle3 ms5.9 ms300
TanStack Virtualestimatedresizemiddle8.2 ms9.3 ms300
Sectile Virtualestimatedresizeend3.2 ms4.3 ms300
Sectile Virtualautomaticresizeend3.1 ms4.2 ms300
TanStack Virtualestimatedresizeend8.2 ms10.2 ms300

Track grid

Comparison conditions
Initial render
Fixed
Estimate provided
Initial render · FixedMax 13 ms
SetupFirst itemsStable layout
Sectile Virtualv0.11.1 · Vue 3.5.225 rounds
12.6 ms
12.6 ms
13.2 ms
react-windowv2.3.0 · React 19.2.85 rounds
2.7 ms
2.7 ms
3.5 ms
Virtuav0.50.5 · React 19.2.8

This condition is unsupported or has no measurement.

View raw measurements

Initial render and scrolling

LibraryInput modeSetupFirst itemsStable layoutScroll medianScroll p95Samples
Sectile Virtualfixed12.6 ms12.6 ms13.2 ms8.7 ms13.5 ms100
react-windowfixed2.7 ms2.7 ms3.5 ms8 ms9.5 ms100
VirtuaestimatedInitial-render failure · geometry:item-37215

Layout mutations

LibraryInput modeOperationLocationScroll medianp95SamplesFailed
Sectile Virtualfixedinsertstart11.4 ms16.6 ms300
react-windowfixedinsertstart7.9 ms8.3 ms300
Sectile Virtualfixedinsertmiddle12.4 ms17.6 ms300
react-windowfixedinsertmiddle7.7 ms8 ms300
Sectile Virtualfixedinsertend9.7 ms14.3 ms300
react-windowfixedinsertend7.5 ms8.4 ms300
Sectile Virtualfixedmovestart9.1 ms13.9 ms300
react-windowfixedmovestart7.8 ms8.8 ms500
Sectile Virtualfixedmovemiddle10.1 ms15.6 ms300
react-windowfixedmovemiddle7.5 ms7.9 ms300
Sectile Virtualfixedmoveend10.2 ms15.6 ms300
react-windowfixedmoveend7.5 ms10.9 ms500
Sectile Virtualfixedremovestart11.3 ms16.6 ms300
react-windowfixedremovestart7.9 ms8.2 ms300
Sectile Virtualfixedremovemiddle12.4 ms18.8 ms300
react-windowfixedremovemiddle7.4 ms8.3 ms300
Sectile Virtualfixedremoveend10.2 ms15.5 ms300
react-windowfixedremoveend7.5 ms8.7 ms300
Sectile Virtualfixedresizestart9.6 ms14.6 ms300
react-windowfixedresizestart7.9 ms8.8 ms300
Sectile Virtualfixedresizemiddle10.7 ms16.1 ms300
react-windowfixedresizemiddle7.5 ms8.3 ms300
Sectile Virtualfixedresizeend9.5 ms14.5 ms300
react-windowfixedresizeend7.5 ms8.4 ms300

Spatial

Comparison conditions
Initial render
Positioned
Initial render · PositionedMax 90 ms
SetupFirst itemsStable layout
Sectile Virtualv0.11.1 · Vue 3.5.225 rounds
90 ms
90 ms
90.4 ms
View raw measurements

Initial render and scrolling

LibraryInput modeSetupFirst itemsStable layoutScroll medianScroll p95Samples
Sectile Virtualpositioned90 ms90 ms90.4 ms7.2 ms12 ms100

Layout mutations

LibraryInput modeOperationLocationScroll medianp95SamplesFailed
Sectile Virtualpositionedinsertstart141 ms168.5 ms300
Sectile Virtualpositionedinsertmiddle100.9 ms130.8 ms300
Sectile Virtualpositionedinsertend8.2 ms9.2 ms300
Sectile Virtualpositionedmovestart8.1 ms9.2 ms300
Sectile Virtualpositionedmovemiddle8 ms9 ms300
Sectile Virtualpositionedmoveend8 ms9.5 ms300
Sectile Virtualpositionedremovestart139.5 ms161.7 ms300
Sectile Virtualpositionedremovemiddle98.9 ms130.7 ms300
Sectile Virtualpositionedremoveend8 ms8.5 ms300
Sectile Virtualpositionedresizestart8.1 ms9 ms300
Sectile Virtualpositionedresizemiddle8 ms9 ms300
Sectile Virtualpositionedresizeend8 ms8.6 ms300

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.

sh
pnpm --filter @sectile/benchmark-virtual-ecosystem dev

Isolated Sectile layout work

The browser suite includes framework and DOM work. This command isolates @sectile/virtual extent indexing, viewport queries, and layout-plan creation:

sh
pnpm --filter @sectile/virtual benchmark

Observed on 2026-08-27 with Node 24.19.0, Apple Silicon, macOS:

OperationMedian time
Extent lookup pair, 100k0.676 µs
Extent lookup pair, 1m0.870 µs
Linear window reduction, 100k1.237 µs
Linear materialized plan, 100k7.694 µs
Linear changed 32-item measurement batch, 100k2.857 µs
Sparse track-grid query, 100k regions13.841 µs
Sparse track-grid changed 32-row batch5.451 µs
Masonry query, 100k items / 8 lanes21.873 µs
Spatial packed-tree query, 100k overlapping items39.982 µs
Strategy100k index construction
sparse track grid45.742 ms
masonry, 8 lanes16.467 ms
spatial packed tree73.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.

Released under the MIT License.