Skip to content

Interactive Benchmarks

ivue vs the World argues that a domain model — thousands of live, individually-reactive entities — has a structural memory cost in idiomatic Vue, and that ivue's laziness erases it. This page is the receipt: a real, reproducible, end-to-end measurement, not a microbenchmark. Performance by Design covers the micro-level numbers (creation of isolated instances, per-read cost); this page covers what happens when you build an actual grid out of them.

The live benchmarks on this page:

The grid: one model, three ways

The first benchmark builds the same 40-column virtualized spreadsheet grid three ways from identical seeded data:

  • Composable — the idiomatic Vue 3 "composable per entity": every cell eagerly allocates a ref() plus four computed()s.
  • ivue — a Reactive() class $Cell: raw is a ref-getter, three derived values are plain getters (0 bytes/instance), and only the one hot value is a computed(). Refs/computeds materialize lazily.
  • POJO floor — plain { row, col, raw } objects, no reactivity at all. The theoretical memory minimum; edits don't re-render, by design.

Try it

Click a button — it builds all three models at once, from the same data, so every number below is directly comparable. Nothing runs until you click: this is a live embed of the shipped engine, not a video.

Best measured result among fully reactive implementations. The POJO control marks the non-reactive floor.

Grid benchmark — one grid, three modelsLive · runs the shipped engine

Nothing built yet — click a button above.

All three arms build from the same seeded 40-column data, at the same time, from the same button click — so every number below is directly comparable. This is a live, in-your-browser illustration; the controlled measurement (gc-forced heap reads, 3-run medians, one fresh page load per arm) lives in demo/grid/RESULTS.md.

The measured numbers

The live demo above is illustrative — same machine, same session, but not gc-forced or run in isolation. The numbers below are the controlled measurement: 3 runs per arm, median reported, headless Chromium, heap read via window.gc() ×3 + performance.memory.usedJSHeapSize, one fresh page load per arm. Full protocol, caveats and raw numbers in demo/grid/RESULTS.md.

100,000 cells (40 cols × 2,500 rows)

ArmModel heap (virtualized)Bytes/cellModel heap (all materialized)Bytes/cell (mat.)Creation
Composable77.3 MB773109.8 MB1,09875.1 ms
ivue5.7 MB5740.3 MB40311.4 ms
POJO floor4.5 MB4510.3 ms

1,000,000 cells (40 cols × 25,000 rows)

ArmModel heap (virtualized)Bytes/cellModel heap (all materialized)Bytes/cell (mat.)Creation
Composable757.7 MB7581,083.7 MB1,084406.8 ms
ivue41.7 MB42388.3 MB38857.2 ms
POJO floor40.5 MB4153.3 ms

The sharpest number: marginal cost per added cell

Scaling 100k → 1M (×10 cells) isolates the fixed viewport/DOM overhead from the per-cell cost. The result:

MetricComposableivuePOJO
Marginal heap per added cell756 B/cell40.0 B/cell40.0 B/cell

ivue's marginal cost matches the non-reactive POJO floor to the byte. An unrendered ivue cell costs exactly what a plain object would have cost — except it is fully reactive: watchable, derivable, editable. At the realistic (virtualized) footprint, ivue holds 13.6× less memory than the composable at 100k and 18.2× less at 1M, while creating the model 6.6× and 7.1× faster, respectively.

Both reactive arms pass live reactivity verification in every run: editing a cell after scrolling away and back re-renders the new value and its row's Σ recomputes. The POJO arm's edit mutates the data but does not re-render — reported, not asserted, since it has no reactivity by design.

The formula grid: real formulas, discovered dependencies

The grids above prove the memory claim with a toy derivation. The formula grid replaces the toy cell with a real one: a working spreadsheet whose cells hold actual Excel-formula syntax=A1+B2, =SUM(A1:D1), =IF(A1>0,B1,C1) — parsed and evaluated by the real fast-formula-parser package (280 Excel-compatible functions, not a stub).

The load-bearing fact: there is no hand-built dependency graph anywhere. Each cell's value is the one computed(); the shared parser's onCell/onRange hooks read referenced cells' value.value while that computed is evaluating, so Vue records those reads as its dependencies. Edit a cell and every dependent formula invalidates and recomputes, cascading through the graph, with no manual invalidation call. This is the formula hole closed: the model stays at full size, at the memory floor, and whole-range formulas just read through it.

And it is not 100k trivial formulas: 52.5% of the 100,000 cells are cross-referencing formulas — arithmetic, SUM/AVERAGE ranges, conditionals, a 50-row running-sum cascade, a cross-column mesh.

Try it

The exact Sheet/FormulaCell model the measurements were run on, live. The parser loads on demand when you click — nothing runs until then. Edit A1 and watch its dependents cascade; select I1 (the IF) and flip A1's sign to watch the tracked dependency set shift branches in the formula bar.

The formula grid — real formulas, liveLive · runs the shipped engine

Nothing built yet. Every cell in columns E–J (and every odd column beyond) holds a real formula — =A1+B1, =SUM(A1:D1), =IF(A1>0,B1,C1), a running sum — evaluated by a real parser, with the dependency graph discovered by Vue.

The model is the exact Sheet/FormulaCell code the measured numbers were produced with; the parser (fast-formula-parser, 280 Excel functions) loads on demand when you click. Live numbers are illustrative — the controlled gc-forced protocol lives in demo/formula/RESULTS.md.

The numbers (3 runs, median, same protocol)

MetricFormula gridToy ivue grid
Model heap (virtualized)8.36 MB5.70 MB
Bytes/cell (virtualized)8457
Model heap (all materialized)48.11 MB40.30 MB
Creation time17.1 ms11.4 ms
VerificationALL PASSpass

At 1M cells (virtualized): 68.41 MB — 68 B/cell, created in 104 ms, with a marginal cost of ~67 B per added cell.

The +27 B/cell over the toy grid is accounted for, not hand-waved: the formula strings themselves (52,500 formulas averaging ~13 chars are simply longer than -142.39 — real formulas are real bytes) and one sheet back-pointer per cell. The materialized delta (+5.15 MB) is the actual dependency graph: each formula computed subscribes to 1–4 other cells' computeds — live cross-cell edges at ~100 B per formula cell. That is the feature, priced honestly. Even carrying real formula text and a live dependency graph, the formula grid virtualizes to 8.4 MB — still ~9× under the composable arm's 77.3 MB for the toy cell.

What the verification proves

Every check runs in Playwright against the live DOM, asserted on every run:

  • Evaluation is correct=SUM(A1:D1), =AVERAGE(A1:D1) and friends produce the arithmetic truth; bad references and 1/0 yield the real #VALUE! / #DIV/0! errors.
  • Conditional dependencies shift. For =IF(A1>0, B1, C1), the dependency set Vue tracks is {A1, C1} while A1 < 0 and provably shifts to {A1, B1} when A1 crosses zero — editing the off-branch cell leaves the formula untouched; editing the live branch moves it.
  • The cascade is live in the DOM. Editing one input updated a running-sum cell five links down the chain in the rendered page, with no manual re-render.

Full write-up, machine notes, honest-cost accounting and caveats: demo/formula/RESULTS.md.

bash
# from the ivue repo root — the interactive playground
npm run dev:demo -- --port 5182                        # then open /grid-formula
npm run measure:formula -- http://localhost:5182       # 100k protocol
npm run measure:formula -- http://localhost:5182 25000 # 1M cells

The flyweight grid: 20 million cells

The grids above price the cell instance. The flyweight pattern removes even that (examples/playground/src/examples/flyweight-grid/, working and measured, structural proofs run at full scale): ground truth lives in columnar typed arrays, cell objects are disposable three-field facades created per render, and reactivity is a sparse overlay that materializes per observation. One invariant governs all of it:

Everything costs proportional to what's observed; nothing costs proportional to what exists.

Run it here — the code loads on demand, then one click creates all twenty million cells in your browser:

The flyweight grid — 20,000,000 cellsLive · runs the shipped engine

20 columns × 1,000,000 rows, fully reactive at 4.7 bytes per cell. Nothing downloads until you click — the model code and the formula parser load on demand, then one more click creates all 20,000,000 cells in your browser.

Ground truth in columnar typed arrays, disposable cell facades per render, a sparse reactive overlay that materializes per observation and evicts with the viewport. ~55% real Excel-syntax formulas. Everything costs proportional to what's observed — never to what exists.

20 columns × 1,000,000 rows = 20,000,000 live cells, ~55% real Excel-syntax formulas, same parser, same gc-forced 3-run protocol:

Metricmeasured (production build)
Model creation, 20,000,000 cells67.1 ms
Model heap89.48 MB
Bytes per cell4.7 B
After 30 viewports across 1M rows+0.3 MB of overlay
Full-column SUM(A1:A1000000)live, 245 block edges

That last pair is the architecture: memory is O(viewport), not O(rows-ever-visited) — viewport-tied eviction returns scrolled-away overlay to the collector, and released cells re-materialize correctly on re-observation. A write to a cell nobody is observing allocates nothing and notifies no one. Development exercises this same production engine path; class or script edits reconstruct the owning example through Vue rather than introducing a benchmark-only dispatch layer.

The per-cell cost ladder

Every rung measured, same machine family, same protocol:

a live cell, at restbytes/cellwhat the cell is
composable (idiomatic Vue)~758closures + eager ref/computeds
ivue instance grid (formula)~67plain object + lazy overlay
plain POJO (no reactivity at all)~40{ row, col, raw }
ivue flyweight columnar4.71 B kind + 8 B Float64, shared

The flyweight sits 8.5× below the "theoretical floor" — because the floor was never plain objects; it was ground truth. A cell at rest is one byte of kind tag plus eight bytes of number, and it is still fully reactive, formula-capable and editable the moment anything observes it. For scale: Google Sheets caps at 10M cells — this 20M-cell document cannot be created there (architectural comparison, not a benchmark).

Status, honestly: a sketch — measured, verified in the DOM down to row 1,000,000, its structural suite green at real scale, but not yet packaged as a shipping layer. The pattern itself — how observation pricing works and where it applies beyond spreadsheets — is The Flyweight Pattern; the raw material lives in the repo: RESULTS.md (measurements, the steady-state memory ceiling, the hot-swap demo), DESIGN.md (mechanisms, including the discovered O(n²) parser-aggregation finding and the columnar fast path), and Flyweight.invariants.md (the structural spec and its impossibility boundary).

bash
# standalone demo, from the ivue repo root
npm run dev:flyweight
npm run measure:flyweight -- <url>

The primitives, interactive

The grids above measure whole models. This one measures the two primitive costs underneath them — creating instances of a real three-level class hierarchy, and calling a prototype-bound method that reads reactive state:

Creation & method dispatch — the primitivesLive · runs the shipped engine
create 100,000 instances
200,000 method calls
InteractiveBox is a three-level Reactive() hierarchy hosting a composable. Creation stays plain-object cheap because nothing materializes until first access; the method benchmark hammers a prototype-bound method with reactive reads inside.

Methodology

Per arm: navigate fresh, wait for the model-not-yet-built state, force window.gc() ×3 and read performance.memory.usedJSHeapSize as a baseline, click create model, measure creation time around the allocation loop only, let the render settle, gc ×3 again and read the heap delta. A second delta forces every cell's derived values to materialize once (the "all materialized" columns), for an apples-to-apples worst case. Repeated 3×, median reported. Full protocol, the machine spec, and every caveat (what the delta does and doesn't include, why heap is bit-stable but creation time varies run-to-run) are in demo/grid/RESULTS.md. The measurement script itself — reusable against your own machine — is demo/grid/measure.mjs:

bash
# from the ivue repo root
npm run dev:demo -- --port 5180
npm run measure:grid -- http://localhost:5180        # 100k cells
npm run measure:grid -- http://localhost:5180 25000  # 1M cells

See also

  • ivue vs the World — why this gap is structural, and why ivue closes it.
  • Performance by Design — the micro-level numbers: isolated instance creation, per-instance memory, hot-loop read cost.

Released under the MIT License.