js-framework-benchmark · keyed implementations
The classic krausest table benchmark — create, update, select, swap, remove and clear 1,000-row tables — reimplemented for Kinetica and run against the same apps in the other frameworks, in one environment, on one machine, with one driver. Extended with 10k-table partial operations, GC accounting, scaling curves, sustained-update frame timing and a deep-tree suite.
One number per framework: how many times slower it is than the fastest contender, averaged (geometric mean) over all 13 DOM operations. 1.00× would mean fastest at everything.
Median duration in milliseconds from the trusted click to the end of the last paint, measured from Chrome traces. The small factor under each value is the slowdown relative to the fastest framework in that row. Hover a cell for the full distribution. Operations 10–13 repeat the partial operations on a 10,000-row table — where accidental O(n²) bookkeeping shows up.
| Kinetica | React | Preact | Vue | Svelte | Vanilla JS | Compose HTML | |
|---|---|---|---|---|---|---|---|
| create 1,000 rows | 39.21.6× | 37.01.5× | 38.51.6× | 29.91.3× | 23.91.0× | 31.81.3× | 226.69.5× |
| replace all 1,000 rows | 41.51.5× | 40.11.5× | 40.01.4× | 32.21.2× | 27.61.0× | 29.81.1× | 337.512.2× |
| partial update (every 10th row) | 7.71.1× | 7.01.0× | 7.31.0× | 7.91.1× | 7.31.0× | 7.21.0× | 12.51.8× |
| select row | 6.81.0× | 6.91.0× | 7.61.1× | 7.31.1× | 7.31.1× | 7.51.1× | 10.41.5× |
| swap two rows | 7.41.0× | 34.84.9× | 8.01.1× | 8.31.2× | 7.11.0× | 7.91.1× | 553.177.5× |
| remove one row | 8.51.2× | 7.21.0× | 7.11.0× | 7.51.1× | 7.11.0× | 7.21.0× | 507.171.4× |
| create 10,000 rows | 278.41.4× | 327.71.6× | 256.01.3× | 232.81.1× | 272.61.3× | 203.01.0× | 7,356.136.2× |
| append 1,000 rows to 1,000 | 41.71.5× | 37.21.3× | 37.31.3× | 27.81.0× | 32.41.2× | 28.21.0× | 226.68.1× |
| clear 1,000 rows | 6.51.0× | 7.21.1× | 6.91.1× | 7.21.1× | 7.01.1× | 6.71.0× | 25.43.9× |
| select row (10k table) | 8.21.0× | 8.31.0× | 7.91.0× | 14.71.8× | 8.21.0× | 8.21.0× | 22.32.8× |
| swap two rows (10k table) | 42.51.5× | 57.62.1× | 33.81.2× | 45.31.6× | 31.01.1× | 27.71.0× | 9,170.7330.8× |
| remove one row (10k table) | 51.81.3× | 43.41.1× | 43.51.1× | 56.51.4× | 40.81.0× | 45.21.1× | 48,296.71,184.0× |
| partial update (every 10th of 10k) | 42.21.4× | 36.91.2× | 37.21.2× | 47.21.6× | 30.11.0× | 31.01.0× | 69.12.3× |
| geometric mean of factors | 1.26× | 1.40× | 1.18× | 1.25× | 1.06× | 1.06× | 16.14× |
Same data as the table, drawn to scale — each chart normalized to its own slowest bar. Lower is better.
Milliseconds spent in V8/Blink garbage collection between the click and the last paint (median; the percentage is the share of the whole operation). High GC share means the operation's cost is allocation pressure, not DOM work — the table only lists operations where some framework pays ≥1ms.
| Kinetica | React | Preact | Vue | Svelte | Vanilla JS | Compose HTML | |
|---|---|---|---|---|---|---|---|
| create 1,000 rows | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 60.5 27% |
| replace all 1,000 rows | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 67.9 20% |
| swap two rows | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 6.3 1% |
| remove one row | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 7.8 2% |
| create 10,000 rows | 81.6 29% | 0.0 0% | 51.1 20% | 52.4 23% | 76.0 28% | 6.6 3% | 823.3 11% |
| append 1,000 rows to 1,000 | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 50.7 22% |
| select row (10k table) | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 6.7 30% |
| swap two rows (10k table) | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 29.4 0% |
| remove one row (10k table) | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 179.2 0% |
| partial update (every 10th of 10k) | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 0.0 0% | 24.7 36% |
The default 1k–20k curve catches complexity-class regressions that a single-size table can't see. The same partial operation is measured at 1k, 2k, 5k, 10k, 20k rows and fitted as duration ∝ nexponent (log-log least squares). Select and swap should be near-flat, update near-linear; a ⚠ cell exceeds its threshold. Hover for the per-size medians.
| Kinetica | React | Preact | Vue | Svelte | Vanilla JS | Compose HTML | |
|---|---|---|---|---|---|---|---|
| select row | n0.13 | n0.07 | n0.13 | n0.41 | n-0.02 | n0.05 | n0.36 |
| swap two rows | n0.74 ⚠ | n0.28 | n0.71 ⚠ | n0.76 ⚠ | n0.65 ⚠ | n0.67 ⚠ | n1.16 ⚠ |
| update every 10th row | n0.74 | n0.74 | n0.76 | n0.78 | n0.67 | n0.73 | n0.65 |
The discrete operations measure a single click; this measures steady-state throughput — the app mutates every 10th of 1,000 rows on every animation frame (dbmonster-style) while a collector records real frame times.
Frame deltas from a requestAnimationFrame collector injected by the driver; the first 500ms are discarded as ramp-up. A steady 120Hz display budget is 8.3ms; >25ms means visibly dropped frames.
A keyed tree of 1555 nested nodes (depth 4, fanout 6) instead of a flat table: creation, re-labelling every 10th leaf, reversing the six top-level subtrees (big keyed moves), and a no-op re-render with unchanged data — the last one isolates pure component/reconciliation overhead, where fine-grained frameworks should approach zero work.
| Kinetica | React | Preact | Vue | Svelte | Vanilla JS | Compose HTML | |
|---|---|---|---|---|---|---|---|
| create tree (1555 nodes) | 18.81.2× | 16.81.1× | 16.51.0× | 21.41.3× | 19.91.2× | 15.91.0× | 54.53.4× |
| update every 10th leaf | 7.21.2× | 6.91.2× | 5.91.0× | 7.11.2× | 7.11.2× | 6.11.0× | 15.82.7× |
| reverse top-level subtrees | 11.21.2× | 11.31.2× | 10.01.0× | 10.01.0× | 9.61.0× | 10.81.1× | 22.52.3× |
| no-op re-render (data unchanged) | 7.41.4× | 5.41.0× | 6.01.1× | 6.91.3× | 6.11.1× | 5.71.0× | 6.21.1× |
| geometric mean of factors | 1.23× | 1.10× | 1.05× | 1.21× | 1.14× | 1.05× | 2.22× |
Replace/create-clear cycles surface retained garbage; the unmount columns surface leaks — the heap after five mount/unmount cycles should match the heap after the first unmount (⚠ marks >0.3 MB growth).
| after load | 1k rows | 5× replace | 10× create+clear | unmounted | 5× remount cycles | |
|---|---|---|---|---|---|---|
| Kinetica | 2.0 | 5.0 | 5.2 | 3.7 | 3.5 | 3.6 |
| React | 1.6 | 4.4 | 5.0 | 3.2 | 2.9 | 2.8 |
| Preact | 1.3 | 4.3 | 4.4 | 2.2 | 2.1 | 2.1 |
| Vue | 1.9 | 4.4 | 4.6 | 2.9 | 2.9 | 2.9 |
| Svelte | 1.3 | 3.2 | 3.4 | 2.4 | 2.4 | 2.4 |
| Vanilla JS | 1.2 | 1.9 | 2.0 | 1.9 | 1.9 | 1.9 |
| Compose HTML | 2.7 | 17.9 | 19.5 | 7.3 | 4.7 | 4.9 |
Kinetica is now measured from the benchmark's esbuild production bundle over the Kotlin/JS linked output, so the browser loads one minified module instead of the toolchain preview's unminified multi-file graph. Remaining payload is mostly the Kotlin runtime surface that the browser renderer imports, plus benchmark app code.
The browser renderer is retained-mode: each dispatch diffs the fresh node tree against a
mounted shadow tree and applies a minimal patch (keyed LIS reconciliation, delegated events,
memoized each rows). Partial operations sit at the paint floor; the remaining gaps
are create-op node construction and allocation pressure — visible directly in the
GC time inside operations section — plus the Kotlin runtime payload that remains
after the benchmark's esbuild bundling step.
The scaling-curve and 10k-table sections exist to catch complexity-class regressions before they reach the headline numbers.
__unmount/__mount hooks with a forced GC between readings.
Sustained updates toggle the app's animate button (every 10th row re-labelled per
rAF) while an injected collector records frame deltas. Scaling curves re-measure select/swap/update
at 1k–20k rows and fit a log-log slope. The deep-tree suite (driver/tree.mjs) runs
create/update/reverse/no-op on a 1,555-node keyed tree.<button> elements rather than
<a>, and it is mounted with KineticaRuntime(debug = false).
React, Preact, Vue and Svelte are minified production builds (Vue includes its runtime template
compiler; op timings are unaffected). Vanilla JS is a direct-DOM baseline with event delegation.
Tree apps use no user-land memoization, so the no-op render measures each framework's honest
re-render cost.