Benchmark: framework comparison

The same js-framework-benchmark row table (see jq79's own numbers), implemented once per framework in frameworks/keyed/ and measured the same way, in the same run, on the same machine: jq79, vanillajs (no framework - the baseline), Preact, Vue, Svelte and React.

Each is idiomatic-but-minimal: hooks for Preact/React, a single-file component for Vue/Svelte/jq79, direct DOM for vanillajs - not tuned beyond what a reasonable implementation would do. 10 runs per operation per framework, each from a fresh page load; the fastest and slowest dropped, the rest averaged.

This is not the official js-framework-benchmark harness (webdriver-ts) - no CPU throttling, no devtools tracing, one machine measuring all six back to back rather than isolated runs. Treat it as directional, not authoritative; the official results table is the one with the CI rigor behind it.

Overall

Geometric mean of each framework's ratio to the fastest framework on each operation - 1.00× would mean "won every single operation"; a plain average of milliseconds would let the two 10,000-row operations drown out the other seven. Lower is better.

vanillajs 1.00× Svelte 3.46× (v5.56.10) jq79 11.35× (v0.7.0) Vue 11.76× (v3.5.42) Preact 15.28× (v10.29.8) React 20.72× (v19.2.8)

Per operation

Median time, colored green (fastest in that row) to red (slowest).

Operationjq79vanillajsPreactVueSvelteReact
Create 1,000 rows35.5 ms5.5 ms17.1 ms16.3 ms8.5 ms17.4 ms
Replace all 1,000 rows38.9 ms12.1 ms32.0 ms23.1 ms15.7 ms27.1 ms
Update every 10th row, 1,000 rows ×1612.1 ms2.9 ms62.6 ms40.6 ms9.3 ms59.6 ms
Select a row, 1,000 rows ×1628.4 ms0.3 ms59.8 ms40.0 ms8.0 ms56.9 ms
Swap rows, 1,000 rows ×1664.3 ms0.3 ms59.8 ms47.3 ms8.2 ms123 ms
Remove a row7.0 ms0.1 ms8.7 ms6.8 ms1.2 ms12.1 ms
Create 10,000 rows291 ms41.1 ms153 ms137 ms66.3 ms418 ms
Update every 10th row, 10,000 rows ×16107 ms24.8 ms815 ms461 ms58.4 ms833 ms
Append 1,000 rows to 10,00067.7 ms6.7 ms77.5 ms63.6 ms12.8 ms199 ms
Clear 10,000 rows119 ms73.5 ms101 ms96.1 ms77.8 ms133 ms

Where the interpreter collects

Updating data in an existing list is what jq79 is fastest at, and it is the same design as the row below. Sixteen updates of every 10th row in a 10,000-row table put it around a fifth of Vue's time and within about 1.7× Svelte - against 3× Svelte on create1k. A write reaches the effects that read the path it changed and nothing else, so an update costs what the changed rows cost rather than what the list costs. There is no diff to run and no component to re-invoke.

Four of these operations are a batch of 16 clicks timed as one measurement, which is the shape the official benchmark writes _x16. A single click on any of them lands under a few milliseconds, and a browser clock that ticks in 0.05ms steps cannot report that honestly.

Where the distance is

jq79 parses and mounts at runtime - no compiler, no virtual DOM - and this table is where that costs something. The numbers behind it have different causes, though, and only one of them is the design.

Building elements is about 3× Svelte at a thousand rows and 4.6× at ten thousand, and that is the price of the design, not a defect in it. A devtools trace of create1k charges 77% of the measured window to JavaScript and finds no layout, no style recalculation and no HTML parsing: jq79 simply executes several times more JavaScript to build the same thousand rows. It is not one hot spot. Four rounds of profiling found one - memoizing the component-name scan, worth about a third - and nothing else above a few percent. The rest is the sum of what a runtime interpreter does per element that compiled straight-line code does not do at all: classify attributes, resolve names through a scope chain, allocate an effect and index its dependencies, build closures. Closing it means deriving that work when the template is parsed, or compiling the template to a closure tree once per component - a different library on the inside, for the same public one.

The list operations are a different story, and an open one. Removing one row of a thousand re-runs 999 rows' bindings, because a dependency is a path (rows.4.label) and a removal changes the path of every row behind it. Swapping two rows runs the whole diff twice - once per write - because jq79 patches the DOM synchronously instead of batching a handler's writes. Selecting a row re-runs 1,000 class bindings, which Svelte does too; there the distance is the cost of one binding - lower since 0.6.5, which resolves a template expression's free identifiers through a declared prologue instead of with, but still an evaluation against a store proxy where Svelte has a compiled read. None of those three is the price of having no compiler.

generated 2026-08-28 · AMD EPYC 9V74 80-Core Processor × 4