js-framework-benchmark · keyed implementations

Kinetica vs React, Preact, Vue, Svelte & vanilla JS

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.

Apple M4 Max darwin arm64 · 36 GB Chromium 149.0.7827.55 (headless) 2026-07-16 10 samples + 3 warmup / op
Kinetica devReact 19.2.7Preact 10.29.4Vue 3.5.39Svelte 5.56.4Vanilla JSCompose HTML 1.11.1

Overall

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.

Geometric mean slowdownacross all measured operations, relative to the fastest framework per operation — lower is better
Svelte 1.06×
Vanilla JS 1.06×
Preact 1.18×
Vue 1.25×
Kinetica 1.26×
React 1.40×
Compose HTML 16.14×

Duration by operation

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.

KineticaReactPreactVueSvelteVanilla JSCompose 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 factors1.26×1.40×1.18×1.25×1.06×1.06×16.14×

Per operation

Same data as the table, drawn to scale — each chart normalized to its own slowest bar. Lower is better.

create 1,000 rows
Kinetica 39.2
React 37.0
Preact 38.5
Vue 29.9
Svelte 23.9
Vanilla JS 31.8
Compose HTML 226.6
replace all 1,000 rows
Kinetica 41.5
React 40.1
Preact 40.0
Vue 32.2
Svelte 27.6
Vanilla JS 29.8
Compose HTML 337.5
partial update (every 10th row)
Kinetica 7.7
React 7.0
Preact 7.3
Vue 7.9
Svelte 7.3
Vanilla JS 7.2
Compose HTML 12.5
select row
Kinetica 6.8
React 6.9
Preact 7.6
Vue 7.3
Svelte 7.3
Vanilla JS 7.5
Compose HTML 10.4
swap two rows
Kinetica 7.4
React 34.8
Preact 8.0
Vue 8.3
Svelte 7.1
Vanilla JS 7.9
Compose HTML 553.1
remove one row
Kinetica 8.5
React 7.2
Preact 7.1
Vue 7.5
Svelte 7.1
Vanilla JS 7.2
Compose HTML 507.1
create 10,000 rows
Kinetica 278.4
React 327.7
Preact 256.0
Vue 232.8
Svelte 272.6
Vanilla JS 203.0
Compose HTML 7,356.1
append 1,000 rows to 1,000
Kinetica 41.7
React 37.2
Preact 37.3
Vue 27.8
Svelte 32.4
Vanilla JS 28.2
Compose HTML 226.6
clear 1,000 rows
Kinetica 6.5
React 7.2
Preact 6.9
Vue 7.2
Svelte 7.0
Vanilla JS 6.7
Compose HTML 25.4
select row (10k table)
Kinetica 8.2
React 8.3
Preact 7.9
Vue 14.7
Svelte 8.2
Vanilla JS 8.2
Compose HTML 22.3
swap two rows (10k table)
Kinetica 42.5
React 57.6
Preact 33.8
Vue 45.3
Svelte 31.0
Vanilla JS 27.7
Compose HTML 9,170.7
remove one row (10k table)
Kinetica 51.8
React 43.4
Preact 43.5
Vue 56.5
Svelte 40.8
Vanilla JS 45.2
Compose HTML 48,296.7
partial update (every 10th of 10k)
Kinetica 42.2
React 36.9
Preact 37.2
Vue 47.2
Svelte 30.1
Vanilla JS 31.0
Compose HTML 69.1

GC time inside operations

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.

KineticaReactPreactVueSvelteVanilla JSCompose 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%

Scaling curves

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.

KineticaReactPreactVueSvelteVanilla JSCompose 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

Sustained updates

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.

p95 frame time under sustained updatesevery 10th row re-labelled per animation frame for several seconds — lower is better
Kinetica 9.3 ms
React 9.3 ms
Preact 9.3 ms
Vue 9.3 ms
Svelte 9.3 ms
Vanilla JS 9.3 ms
Compose HTML 10.1 ms

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.

Deep tree

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.

KineticaReactPreactVueSvelteVanilla JSCompose 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 factors1.23×1.10×1.05×1.21×1.14×1.05×2.22×

Startup & memory

JS payload (gzip)kilobytes of JavaScript shipped to render the app
Kinetica 86 KB
React 60 KB
Preact 9 KB
Vue 65 KB
Svelte 21 KB
Vanilla JS 1 KB
Compose HTML 172 KB
Time to interactivenavigation → toolbar rendered, local server, median of 5 cold loads
Kinetica 34.4 ms
React 40.6 ms
Preact 27.1 ms
Vue 37.5 ms
Svelte 27.6 ms
Vanilla JS 25.9 ms
Compose HTML 41.5 ms
Script compile + evaluatemerged compile/evaluate intervals from a traced cold load
Kinetica 9.0 ms
React 14.5 ms
Preact 8.8 ms
Vue 9.5 ms
Svelte 5.1 ms
Vanilla JS 6.9 ms
Compose HTML 16.8 ms
JS heap after creating 1,000 rowsforced GC, then JSHeapUsedSize
Kinetica 5.0 MB
React 4.4 MB
Preact 4.3 MB
Vue 4.4 MB
Svelte 3.2 MB
Vanilla JS 1.9 MB
Compose HTML 17.9 MB

Heap across churn & unmount (MB, forced GC)

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 load1k rows5× replace10× create+clearunmounted5× remount cycles
Kinetica2.05.05.23.73.53.6
React1.64.45.03.22.92.8
Preact1.34.34.42.22.12.1
Vue1.94.44.62.92.92.9
Svelte1.33.23.42.42.42.4
Vanilla JS1.21.92.01.91.91.9
Compose HTML2.717.919.57.34.74.9

Why Kinetica ships 86 KB & 1 files

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.

Reading the results

Where Kinetica stands after the renderer rewrite

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.

Methodology

Benchmark suite
The keyed scenario of js-framework-benchmark (krausest): the app is a table of rows (id, label link that selects, remove icon); operations are create 1,000 / replace 1,000 / partial-update every 10th / select / swap rows 2↔999 / remove one / create 10,000 / append 1,000 / clear, plus the same partial operations on the 10,000-row table. Labels come from the benchmark's standard adjective-colour-noun generator.
Measurement
Playwright trusted clicks; per-operation Chrome trace (devtools.timeline); duration = click EventDispatch start -> end of last Paint/Commit event; headless Chrome for Testing; local static server. No CPU throttling. Each operation: 3 warmup runs, then 10 measured samples; the table reports medians. State is reset between samples so every measured click does identical work. GC time sums V8/Blink collection events inside the measured window of the same trace.
Extended suites
Startup script time and blocking time come from two additional traced cold loads. Memory churn clicks through 5× replace and 10× create+clear cycles, then exercises each app's __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.
Fairness
All apps render the same DOM structure (table/tr/td, same CSS file, no hover styles) from the same page shell on the same local server. Kinetica differs only where its DSL requires it: selectable labels and remove controls are <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.
Environment
Apple M4 Max, 36 GB RAM, darwin/arm64, Chromium 149.0.7827.55 headless, no CPU throttling, one framework at a time.