Live benchmark

Cerious-Scroll performance benchmark

Everything on this page is measured in your browser when you press Run. Nothing is pre-recorded. The harness drives the same engine the demos use and reports per-frame cost, mount time, random-access latency, DOM footprint and heap.

1

Configure

Pick one row template and the dataset sizes to walk through. If the engine is doing its job, every number in the results stays flat as the row count climbs.

Row template
Scroll sample per size
Fling velocity
Mount repetitions

Dataset sizes
2

Run

The surface below is the engine under test. It stays visible and interactive the whole time, so you can watch each phase happen and grab it yourself once the run finishes.

Idle. Press Run to start.
idle
3

Results

Nothing measured yet. Run the sweep above and the numbers land here.

Waiting for a run.

Every dataset size

Rows Mount Frame p50 Frame p95 FPS Worst frame Jank Jump p95 DOM nodes Heap
No scale sweep has been run yet.

Click any row to select it. The Frame timeline tab follows your selection.

4

Export

Every export carries the full environment block, browser, cores, device memory, DPR and measured refresh rate, alongside the numbers from both sweeps. A result should never be quoted without the machine that produced it.

How it works what is measured, and what it is not

  1. Refresh-rate calibration. Before anything runs, the harness samples idle requestAnimationFrame intervals and takes the median to derive your display's refresh rate. That sets the frame budget behind the jank and FPS columns, so on a 120 Hz panel the budget works out at 8.3 ms rather than 16.7 ms.
  2. Warm-up, discarded. Each case gets a short scroll burst whose frames are thrown away, so JIT warm-up and first-paint costs never land in the reported numbers.
  3. Mount. Constructs a fresh CeriousScroll, renders the first viewport and forces a layout flush. Repeated n times; the median is reported, which is far more stable than a mean under background noise.
  4. Sustained scroll. A rAF loop applies a fixed pixel delta through scroll(delta, viewportHeight) and repaints via renderViewport(), bouncing at either end of the dataset. Two things are recorded per frame: the frame interval, the wall-clock gap between frames, which carries browser layout, paint and compositing with it; and the JS work the library itself spent. FPS comes from the intervals, while frame p50/p95 report the library's own cost.
  5. Random access. Jumps to uniformly random indices across the whole dataset via jumpToElement(), repaints, and forces layout. This is the "seek to row 7,431,902" case, and it is where true virtualization parts company with DOM clustering.
  6. DOM footprint. Counts every element resident inside the scroll surface at steady state. This is the O(1) claim in its most falsifiable form: the count must not grow with the dataset.
  7. Row complexity. The complexity sweep pins the dataset size and walks the row template instead. There is a plain card, a twelve-column grid row with a sparkline, a media card with an aspect-ratio thumbnail, star rating and avatar stack, an inline <svg> chart built from gridlines, bars, an area path and a polyline, a row of native form controls, a sixteen-level-deep tree, and a panel carrying gradients, drop shadows and a transform. Every one of them is built from local DOM only, with no network images and no remote fonts, so a run stays deterministic and works offline. The node count on each chip is measured rather than asserted: five rows go into a detached, off-screen probe and the elements are counted.
  8. The control group. With the baseline enabled, every dataset size is also measured against a plain scrollable <div> holding every row, which is what you get without a virtualiser. It is built the way a competent developer would build it, one DocumentFragment and one append, and it renders the identical row template, so it is a fair control rather than a strawman. Two safety limits apply, and both are reported as results rather than hidden: a size needing more than two million DOM nodes is refused outright, and a build that has not finished within ten seconds is abandoned with the row count it reached. One caveat matters for reading the numbers: per-frame JS work is not comparable between the two, because the unvirtualized list does almost nothing in JavaScript per frame while the browser pays the cost at layout and paint. The comparison table therefore comes down to wall-clock frame interval, mount time, and resident DOM.
  9. Engine cost against renderer cost. The harness counts renderer invocations during the measured scroll. Renders / frame is how many rows genuinely entered the viewport on that frame, and it should stay in the low single digits no matter how large the dataset gets. Per row divides the total per-frame JS by those invocations to give the average cost of building one row. Read together, they tell you which side of the boundary your frame budget is going to: the virtualiser, or your own row template.
Honest caveats. FPS is capped by your display. A 60 Hz panel cannot report much beyond 60, so frame p50/p95, the library's own JS cost, is the number that actually measures headroom. Heap readings come from performance.memory, which is Chromium-only, quantised, and at the mercy of garbage collection timing, so treat it as an order-of-magnitude signal rather than a precise figure. Sub-millisecond results sit close to the floor of the clock itself, since browsers quantise performance.now() to roughly 100 µs; a 0.30 ms frame and a 0.35 ms frame are not meaningfully different, so raise the fling velocity or reach for a heavier row template when you want separation. Numbers also move with background tabs, battery saver, thermal state and extensions, so close the noisy ones before quoting anything. And remember the harness measures the scroller together with whatever renderer you point it at: a heavier row template lifts every column, which is exactly why the template is a knob you can turn.
Full environment readout