Local benchmark harness for comparing JavaScript reactivity libraries as behavior profiles across several scenario types:
- classic propagation graphs
- computation creation and update tests
- larger static and dynamic dependency graphs
The project bundles the runner with esbuild, runs benchmarks under node --expose-gc, saves the raw log, and generates an HTML results page. The report ranks by relative geometric mean and keeps per-test medians, p95/p99 tails, and workload families visible.
pnpm install
pnpm benchTo run only one framework locally, set BENCH_FRAMEWORK:
BENCH_FRAMEWORK=ripple pnpm benchYou can also pass a comma-separated list such as BENCH_FRAMEWORK="ripple,alien-signals".
By default each benchmark row uses one measured run so the full suite stays practical for local iteration. To collect stronger distribution statistics, set BENCH_RUNS:
BENCH_RUNS=30 pnpm benchAfter the run, these files are updated:
bench-results/latest.log- raw runner outputindex.html- generated summary page with rankings and per-test comparisons
If you already have a log and only want to rebuild the results page:
pnpm results:pagepnpm test- runvitestpnpm build- bundlesrc/index.tsintodist/pnpm run- execute the built runner withnode --expose-gcpnpm bench- build, run benchmarks, savebench-results/latest.log, and refreshindex.htmlpnpm results:page- rebuildindex.htmlfrom an existing log
The current default run includes several benchmark groups. They are classified by workload family instead of being treated as one flat race:
creation- signal/computed allocation and setup costupdate- stable graph writes and invalidation hot pathspull- dirty reads, aggregation, and chain pullspush- fan-out propagation and effect deliverydynamic- branch switching, dependency cleanup, and retrackinglarge_graph- scaling behavior on larger DAGsbaseline- historical propagation scenarios such asdiamond,mux, andtriangle
The dynamicBench set now also includes several more app-like presets such as
dashboard selective reads, editor derived state, kanban board, and
entity detail page. These use the same rectangular graph generator, but with
more moderate widths/depths and partial leaf reads to better resemble common UI
workloads.
The larger graph benchmark parameters are defined in src/config.ts.
The semantics and interpretation rules are documented in SEMANTICS.md. Read that file before treating results as comparable across libraries.
Rows are measured according to BENCH_RUNS (1 by default for speed). The primary terminal time column is the median, not the fastest run. Rows also emit:
p95andp99- tail latency for jitter/outlierscv- coefficient of variation for stabilitygroupandfamily- workload taxonomy- benchmark-specific counters such as recomputation, traversal, or checksum data
The HTML leaderboard uses:
r(L, test) = median_time(L, test) / best_median_time(test)
score(L) = geometric_mean(r(L, tests))Lower is better. A profile score is more honest than an arithmetic average because a catastrophic slowdown in one workload family is harder to hide.
The repository contains adapters for:
- Reflex
- Alien Signals
- Angular Signals
- MobX
- mol_wire
- Oby
- Preact Signals
- Reactively
- S.js
- Signia
- Solid
- @solidjs/signals
- Svelte
- TC39 Signals Proposal polyfill
- Tansu
- uSignal
- Vue Reactivity
- Compostate
- Valtio
- Kairo
The list of adapters enabled in the default run is defined in src/config.ts.
Two adapter notes are worth calling out explicitly:
Svelte, TC39 Signals Proposal polyfill and Angular Signals is currently removed from the default benchmark set because it was too slow for the current suite, especially once the larger graph scenarios were added.
index.html is generated by scripts/render-results-page.mjs from the CSV-like text output produced by the benchmark runner.
- src/index.ts - runner entry point
- src/config.ts - active framework list and large-graph test configuration
- scripts/bench-and-update.mjs - full benchmark-and-refresh pipeline
- scripts/render-results-page.mjs - log parser and profile-oriented HTML report generator
- src/frameworks.test.ts - adapter sanity checks with
vitest
- Some adapters are disabled because parts of the suite currently hang or are incompatible. The reasons are documented inline in src/config.ts and src/index.ts.
bench-results/latest.logandindex.htmlalways reflect the last local run. If the active framework list changed, those artifacts may still describe an older run until you executepnpm benchagain.
Based on the original idea and structure from milomg/js-reactivity-benchmark.