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 librariesreact+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.

Operationotfwreactsolidsvelte
create 1,000 rows261.1274.1244.3257.6
create 10,000 rows2627.83338.62542.22548.8
append 1,000 to 1,000295.8286.1270.1267.4
update every 10th (1k)91.893.8108.992.5
swap 2 rows (1k)33246.431.131.3
select row (1k)26.127.33029
remove row (1k)40.439.937.536.1
clear 10,000 rows178.6260.9164165.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.

These operations are precisely where fine-grained libraries are already at parity with OTF Web, which is why seven of eight rows tie. The properties that distinguish it as a framework — native custom elements, hydration by adoption, per-route chunks — show up in JavaScript shipped per route, cold start, hydration cost and SPA navigation, none of which this benchmark measures. Treat it as evidence about the rendering layer and nothing more.

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.

Metricotfwastronexttanstackvite
Peak build memory (MB)1404161226510260
Build time (s)0.31.23.820.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.md

Full method, every caveat, and the complete size ladder live in the repository: benchmarks/README.md and benchmarks/ssg-build.