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.
Per operation
Median time, colored green (fastest in that row) to red (slowest).
| Operation | jq79 | vanillajs | Preact | Vue | Svelte | React |
|---|---|---|---|---|---|---|
| Create 1,000 rows | 35.5 ms | 5.5 ms | 17.1 ms | 16.3 ms | 8.5 ms | 17.4 ms |
| Replace all 1,000 rows | 38.9 ms | 12.1 ms | 32.0 ms | 23.1 ms | 15.7 ms | 27.1 ms |
| Update every 10th row, 1,000 rows ×16 | 12.1 ms | 2.9 ms | 62.6 ms | 40.6 ms | 9.3 ms | 59.6 ms |
| Select a row, 1,000 rows ×16 | 28.4 ms | 0.3 ms | 59.8 ms | 40.0 ms | 8.0 ms | 56.9 ms |
| Swap rows, 1,000 rows ×16 | 64.3 ms | 0.3 ms | 59.8 ms | 47.3 ms | 8.2 ms | 123 ms |
| Remove a row | 7.0 ms | 0.1 ms | 8.7 ms | 6.8 ms | 1.2 ms | 12.1 ms |
| Create 10,000 rows | 291 ms | 41.1 ms | 153 ms | 137 ms | 66.3 ms | 418 ms |
| Update every 10th row, 10,000 rows ×16 | 107 ms | 24.8 ms | 815 ms | 461 ms | 58.4 ms | 833 ms |
| Append 1,000 rows to 10,000 | 67.7 ms | 6.7 ms | 77.5 ms | 63.6 ms | 12.8 ms | 199 ms |
| Clear 10,000 rows | 119 ms | 73.5 ms | 101 ms | 96.1 ms | 77.8 ms | 133 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