Kinetica

Performance

Kinetica is benchmarked continuously against React, Preact, Vue, Svelte and a vanilla-JS baseline using a local reimplementation of the js-framework-benchmark (krausest) keyed suite: identical table apps, one machine, one Chromium, one Playwright/Chrome-trace driver. Duration = trusted click → end of last paint.

Current benchmark results (2026-07-16)

Generated by node bench/run.mjs --update-docs; do not edit this block manually.

Median click-to-paint duration in milliseconds unless shown as seconds. Environment: Apple M4 Max, darwin/arm64, Chromium 149.0.7827.55, 3 warmups + 10 measured samples.

OperationKineticaReactPreactVueSvelteVanilla JSCompose HTML
create 1,000 rows39.237.038.529.923.931.8226.6
replace all 1,000 rows41.540.140.032.227.629.8337.5
partial update (every 10th row)7.77.07.37.87.37.212.5
select row6.86.97.57.37.37.510.4
swap two rows7.434.88.08.37.17.9553.1
remove one row8.57.27.17.57.17.2507.1
create 10,000 rows278.4327.7256.0232.8272.6203.07.36 s
append 1,000 rows to 1,00041.737.237.327.832.428.2226.6
clear 1,000 rows6.57.26.97.27.06.725.4
select row (10k table)8.28.37.914.78.28.222.3
swap two rows (10k table)42.557.633.845.330.927.79.17 s
remove one row (10k table)51.843.443.556.540.845.248.30 s
partial update (every 10th of 10k)42.236.937.247.230.131.069.1
geometric mean vs per-operation fastest1.26×1.40×1.18×1.25×1.06×1.06×16.14×

Compared with accepted (no complete versioned run yet): 70 regressions, 102 improvements, and 174 metrics within the 5% threshold. Open the comparison report.

Raw data: results.json · tree.json · scaling.json · JVM results · size/build metrics.

Live demos

Every benchmarked framework is hosted live at /bench/, unmodified — the same production bundles the numbers above come from.

A heads-up before you click around the Compose HTML page: its swap/remove on large keyed lists scale close to O(n²), not a fluke of one run — clicking "Create 10,000 rows" then "Remove" will visibly hang the tab for tens of seconds.

How it got there

The July 2026 rewrite moved the original 9-op geometric mean from 15.2× to roughly 1.1×; frame ordinals then took the 13-op headline below React for the first time. These are historical checkpoints; the generated table above is the source of truth for current numbers.

  1. Event registry fix (15.2× → 3.0×). Profiling showed 60–67% of all CPU in an O(n) identity scan run per handler per render — O(n²) per operation and leaking. A hash map with commit-time eviction removed it; create-10k went from 20.3 s to 0.4 s.
  2. Retained renderer (3.0× → 1.93×). The original renderer rebuilt the entire DOM on every event. The current renderer diffs against a mounted shadow tree: keyed LIS reconciliation, prop-level patches, root event delegation, focus preserved.
  3. Debug costs gated. Journal sampling, path attributes and focus bookkeeping only run with debug = true.
  4. Keyed row memoization (1.93× → 1.42×). each caches every row's output by item equality, tracked cell versions and context reads; unchanged rows re-emit the same Node references, which the diff skips with one identity comparison. Remove-row dropped from 19 ms to 9 ms, select to the paint floor.
  5. Allocation hygiene (1.42× → 1.29×). The remaining create-path cost was Kotlin stdlib map/hash work: props for host nodes are now flat arrays (propsOf) instead of hash maps, host() stops copying the props map, and slot/event key derivation reuses an incrementally maintained key-scope prefix. Create-10k dropped from 474 ms to roughly 300 ms.
  6. Browser fast-paths + benchmark bundling. The browser renderer avoids repeated tag/attribute classification work, and the benchmark now loads Kinetica through a post-link esbuild production bundle instead of the Kotlin Toolchain preview multi-file graph. Startup dropped to 22.6 ms at this phase with one JS file and an 82 KB gzip payload (24.5 ms / 85 KB after frame ordinals).
  7. Frame ordinals (1.20× → 0.97×). The compiler plugin became mandatory and slot identity moved to compile time: every state/derived/effect call site gets a static ordinal in a per-component frame, so the hot path reads slots by array index — no key strings, no metadata maps — and commit-time eviction touches only re-rendered frames instead of scanning all slots and events per render. The 13-op geomean crossed below React for the first time: swap1k 0.36×, swap10k 0.77×, replace1k 0.88× and create10k 0.94×; remove10k (1.31×) and update-every-10th-10k (1.24×) are the remaining DOM open items.

The soundness fixes that followed traded roughly 8 ms of startup and 1.9 MB of after-1k heap for correctness. The cost is on the mount path, not the child diff: the mixed static/keyed reconciler runs only while patching updates, and reworking it from a browser-side heuristic into runtime-declared region spans (the runtime marks each each/conditional region; the browser reconciles by those boundaries) measured startup- and heap-neutral in a same-branch A/B. The regression is the mount-time work the other fixes added — per-subscription cell-listener holders and cumulative bundle growth — and regaining it is tracked as an open item. The DOM-operation geomean was unaffected. The full root-cause analysis, per-step measurements and methodology live in the repository's git history.

What the suite measures

Beyond the classic nine operations, the harness tracks the regressions that a single-size, single-click table can't see:

  • 10k-table partial operations — select/swap/remove/update against 10,000 rows, where accidental O(n²) bookkeeping surfaces while the 1k numbers still look healthy.
  • GC accounting — time spent in V8/Blink collection inside each measured operation, read from the same Chrome traces; separates allocation pressure from DOM work.
  • Scaling curves — the same operation at 1k–20k rows fitted as duration ∝ n^exponent, with per-operation thresholds that flag superlinear behaviour.
  • Sustained updates — a dbmonster-style animation loop (every 10th row re-labelled per frame) measured as fps, p95 frame time and dropped-frame share, not single-click latency.
  • Deep tree — a 1,555-node keyed tree (create, leaf updates, subtree moves, and a no-op re-render that isolates pure reconciliation overhead per framework).
  • Memory churn and leaks — heap after replace/create-clear cycles and after repeated mount/unmount cycles through each app's teardown path. This gate is an open regression: heap after 1k rows is 7.6 MB against a ≤5 MB target (React 4.4 MB, vanilla 1.9 MB), up from 5.7 MB before the soundness fixes added retained per-row state.
  • CPU-throttled pass — the whole suite under 4× CDP throttling as a low-end-device proxy.
  • JVM microbenchmarks (bench-jvm module) — reactive-core propagation, render-pipeline Node construction and markdown SSR throughput, minutes-fast for per-PR regression checks.
  • Bundle-size baseline — CI fails when a tracked artifact's gzip size grows >10% over bench/size-baseline.json.

Performance model

  • An event costs: handler + node-tree build + diff O(tree) + patch O(changes) + paint.
  • Unchanged subtrees compare cheaply; reference-equal subtrees (memoized each rows, skippableNode) short-circuit the diff in O(1), so a partial update builds only the changed rows' Nodes.
  • clear-style teardowns and paint-bound operations run at the vanilla-JS floor already.
  • 10k-table partial operations and their scaling exponents are tracked in every complete run; inspect the generated report and comparison rather than copying point-in-time values here.
  • The benchmark payload uses a post-link esbuild bundle over Kotlin/JS output while the Kotlin Toolchain js/app product is still a preview. Startup and gzip payload are captured in the accepted run and compared automatically with its baseline.

Reproducing

node bench/run.mjs                         # provision, build and run everything
node bench/run.mjs --compare-to=accepted  # render JSON + HTML deltas against published data
node bench/run.mjs --update-docs           # accept a complete run and refresh this page

Benchmark on AC power only: below ~10% battery, macOS low-power throttling floors every headless-Chrome operation at ~30 ms for every framework. The harness, app contract, and how to add another framework are documented in bench/README.md.