Renderer Benchmarks
The same three storefront pages, the same props, rendered three ways. Every run asserts the output is byte-identical before it reports a number, so the comparison is between renderers and not between outputs.
Warm render
| Page | React in QuickJS | React in V8 | FSR | FSR + mimalloc | vs QuickJS |
|---|---|---|---|---|---|
| Catalog (12 cards) | 1.86 ms | 132 µs | 128 µs | 82 µs | 22.6x |
| Product page | 369 µs | 18.2 µs | 18.0 µs | 12.5 µs | 29.4x |
| Cart (3 items) | 426 µs | 11.5 µs | 19.5 µs | 12.7 µs | 33.5x |
Two FSR columns because the allocator is a deployment choice worth about 1.5x and nothing in the library or the host picks one for you. QuickJS is quoted with the same allocator as FSR; it barely moves either way, because the engine’s own allocation dominates.
How it scales
The same catalogue route rendered at four list lengths, against React in V8 on the same props, with the output checked byte-identical at every length.
| Products | React in V8 | FSR | Lead |
|---|---|---|---|
| 1 | 20.0 µs | 15.1 µs | 1.32x |
| 12 | 135 µs | 82.2 µs | 1.64x |
| 100 | 1.05 ms | 615 µs | 1.71x |
| 1000 | 10.9 ms | 6.21 ms | 1.76x |
Both are linear across three orders of magnitude and the lead widens with size, so it is a property of the design rather than of one page. Per product the cost settles at 6.15 µs for FSR against 10.51 µs for V8. The single-item row is fixed page overhead that amortises away.
Under load
Several requesters rendering the catalogue flat out for ten seconds each, counting what came out. React server rendering is synchronous work an event loop cannot interleave, so Node is shown both ways it can use more than one core: isolates inside one process and a process per requester.
Renders per second, higher is better:
| Requesters | QuickJS | V8, worker threads | V8, process each | FSR |
|---|---|---|---|---|
| 1 | 538 | 7,442 | 7,481 | 12,033 |
| 2 | 867 | 11,357 | 14,243 | 23,436 |
| 4 | 1,734 | 11,708 | 27,895 | 40,694 |
| 8 | 3,305 | 5,549 | 54,353 | 74,606 |
What each request waits while the others compete, lower is better:
| Requesters | QuickJS | V8, worker threads | V8, process each | FSR |
|---|---|---|---|---|
| 1 | 1.86 ms | 134 µs | 134 µs | 83 µs |
| 8 | 2.42 ms | 1.44 ms | 147 µs | 107 µs |
Worker threads peak at four and then go backwards, so the process column is the fair comparison, where FSR leads by 1.37x. Node processes take more resources overall, with independent garbage collectors.
Cold start
| Bringing up an engine | Catalog | Product | Cart |
|---|---|---|---|
| QuickJS context, warm process | 29.3 ms | 29.4 ms | 28.4 ms |
| Node process and module graph | 68.5 ms | 68.4 ms | 57.9 ms |
These two are different units and neither answers the other. FSR pays neither: there is no engine in the serving path, so a render starts on a tree the build already lowered.
How to read this
These pages are markup-heavy. They loop over data and emit tags, which is the work a server renderer actually does most of and it is where an interpreter walking a lowered tree does less than a JIT that must first build JavaScript objects to walk. A component doing real arithmetic is where a JIT wins instead and no such page is measured here.
Apple M4 Pro, high power mode. Criterion for the Rust and QuickJS rows; the V8 rows are Node v26.8.1, 2000 samples after 500 warmups. Measured 2026-09-07.