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.
| Operation | Kinetica | React | Preact | Vue | Svelte | Vanilla JS | Compose HTML |
|---|---|---|---|---|---|---|---|
| create 1,000 rows | 39.2 | 37.0 | 38.5 | 29.9 | 23.9 | 31.8 | 226.6 |
| replace all 1,000 rows | 41.5 | 40.1 | 40.0 | 32.2 | 27.6 | 29.8 | 337.5 |
| partial update (every 10th row) | 7.7 | 7.0 | 7.3 | 7.8 | 7.3 | 7.2 | 12.5 |
| select row | 6.8 | 6.9 | 7.5 | 7.3 | 7.3 | 7.5 | 10.4 |
| swap two rows | 7.4 | 34.8 | 8.0 | 8.3 | 7.1 | 7.9 | 553.1 |
| remove one row | 8.5 | 7.2 | 7.1 | 7.5 | 7.1 | 7.2 | 507.1 |
| create 10,000 rows | 278.4 | 327.7 | 256.0 | 232.8 | 272.6 | 203.0 | 7.36 s |
| append 1,000 rows to 1,000 | 41.7 | 37.2 | 37.3 | 27.8 | 32.4 | 28.2 | 226.6 |
| clear 1,000 rows | 6.5 | 7.2 | 6.9 | 7.2 | 7.0 | 6.7 | 25.4 |
| select row (10k table) | 8.2 | 8.3 | 7.9 | 14.7 | 8.2 | 8.2 | 22.3 |
| swap two rows (10k table) | 42.5 | 57.6 | 33.8 | 45.3 | 30.9 | 27.7 | 9.17 s |
| remove one row (10k table) | 51.8 | 43.4 | 43.5 | 56.5 | 40.8 | 45.2 | 48.30 s |
| partial update (every 10th of 10k) | 42.2 | 36.9 | 37.2 | 47.2 | 30.1 | 31.0 | 69.1 |
| geometric mean vs per-operation fastest | 1.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.
- Full comparison report — the interactive charts
bench/report/generate.mjsrenders locally, published as a static page. - Table apps — Kinetica · React · Preact · Vue · Svelte · Vanilla JS · Compose HTML
- Tree apps (1,555-node keyed tree) — Kinetica · React · Preact · Vue · Svelte · Vanilla JS · Compose HTML
- Raw numbers — results.json · tree.json · scaling.json
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.
- 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.
- 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.
- Debug costs gated. Journal sampling, path attributes and focus bookkeeping only run with
debug = true. - Keyed row memoization (1.93× → 1.42×).
eachcaches 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. - 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. - 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).
- 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-jvmmodule) — 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
eachrows,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/appproduct 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 pageBenchmark 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.