Benchmarks
Two independent measurements. Neither says anything about the other, and each compares a different category — read the framing before the figures.
Runtime — a rendering-layer comparison
This measures OTF Web’s runtime against the React, Solid and Svelte 5 libraries — react+react-dom, solid-js, svelte. It is not a comparison against Next.js, SolidStart or SvelteKit. The case is a single page of reactive list updates, so no router, build step or SSG code runs on any side.
| Operation | otfw | react | solid | svelte |
|---|---|---|---|---|
| create 1,000 rows | 261.1 | 274.1 | 244.3 | 257.6 |
| create 10,000 rows | 2627.8 | 3338.6 | 2542.2 | 2548.8 |
| append 1,000 to 1,000 | 295.8 | 286.1 | 270.1 | 267.4 |
| update every 10th (1k) | 91.8 | 93.8 | 108.9 | 92.5 |
| swap 2 rows (1k) | 33 | 246.4 | 31.1 | 31.3 |
| select row (1k) | 26.1 | 27.3 | 30 | 29 |
| remove row (1k) | 40.4 | 39.9 | 37.5 | 36.1 |
| clear 10,000 rows | 178.6 | 260.9 | 164 | 165.3 |
OTF Web leads no operation here. Solid takes create 1,000 rows; the other seven rows are ties. It is well clear of React on swap 2 rows (33 vs 246) and clear 10,000 (179 vs 261).
Method
The standard js-framework-benchmark operation set, through one shared harness: production builds, 4× CPU throttling applied over CDP before the page loads, and each operation timed by a doublerequestAnimationFrame around one synchronous state write. Every case uses keyed rows and the identical measure/nextFrame helpers and DOM shape.
Why the figures pool several runs
Double-rAF timing quantizes to frame boundaries, giving a resolution of about 8.3 ms — and on the create rows OTF Web, Solid and Svelte sit within roughly 20 ms of one another. A single run’s margins there are smaller than its own run-to-run drift: three consecutive runs on an idle machine named three different winners for create 1,000 rows, and Solid’s own median swung 13% (231–262 ms) across them. Bolding any one run would report a coin flip, so the published table pools the raw samples of 3 full runs and takes the median of the pool — the same statistic, at N≈30 rather than N≈10. A cell is bolded only where the margin exceeds the timing resolution; most do not.
What is and is not fair here
The asymmetry between a framework and a view library is real, but it lands outside the measured window. The OTF Web case is built with the full otfw build toolchain and boots the router and mount layer; the other three are minimal hand-rolled bundles with no router. That cost is paid once at startup and never enters a timed sample, so it neither inflates nor deflates the per-operation medians — but it does mean OTF Web carries weight the others do not. For the same reason, a bundle-size column would not belong in this table.
React’s swap 2 rows result is a genuine characteristic rather than a handicapped implementation: plain keyed React re-renders and reconciles the whole list, and the reference benchmark’s baseline React entry is likewise unmemoized. Memoizing rows would close much of that gap.
Build cost — a framework-level comparison
This one compares frameworks: Astro, Next.js and TanStack Start are full frameworks, and Vite is the underlying build tool included as a floor. It measures the cost of building a static site, not of serving one — five toolchains pre-rendering the same 72 KB MDX documentation page.
| Metric | otfw | astro | next | tanstack | vite |
|---|---|---|---|---|---|
| Peak build memory (MB) | 140 | 416 | 1226 | 510 | 260 |
| Build time (s) | 0.3 | 1.2 | 3.8 | 2 | 0.8 |
Read the asymmetries first
The columns do not represent equal work. astro and vite ship no client JavaScript for these pages, whereas OTF Web, Next.js and TanStack Start additionally build a hydration bundle. Two smaller asymmetries run the other way, against OTF Web: it is the only tool that syntax-highlights the code blocks at build time, and Next.js inlines an RSC payload the others have no equivalent of.
The figures also come from two separate runs. The OTF Web column was re-measured after the build fixes in @opentf/web-compiler and re-verified across the whole 72 KB–2.3 MB ladder; the other four are from the original sweep and were not re-run, so small gaps should be treated as noise. The 72 KB memory cell read 122–140 MB between two consecutive runs, which is this benchmark’s noise floor for that cell.
The advantage holds across the size ladder — a 2.3 MB page builds in 2.9 s against Astro’s 3.7 s — where it previously reversed beyond roughly a megabyte.
Reproduce them
# runtime — repeat a few times, then pool
bun run bench all
bun benchmarks/aggregate.mjs --latest 3
# build cost — see benchmarks/ssg-build/README.mdFull method, every caveat, and the complete size ladder live in the repository: benchmarks/README.md and benchmarks/ssg-build.