Renderer Benchmarks
Three storefront pages are rendered three ways with the same props. Every run asserts the output is byte-identical before it reports a number, so every renderer produced the same HTML.
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 |
FSR has two columns because the allocator changes its speed by about 1.5x and you choose the allocator when you deploy. Neither the library nor the host picks one. QuickJS uses the same allocator as FSR. Its numbers barely change with the allocator, because most of its allocation happens inside the engine.
How it scales
This renders the same catalogue route 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 scale linearly across three orders of magnitude and FSR’s lead grows with list size, so the lead does not depend on one particular page. Each product costs 6.15 µs in FSR and 10.51 µs in V8. The single-item row is mostly fixed per-page overhead, which matters less as the list grows.
Under load
Several requesters render the catalogue as fast as they can for ten seconds and count the completed renders. 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-thread throughput peaks at four requesters and then drops, so compare FSR against the process column, where FSR is 1.37x faster. 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 |
The two rows measure different things, so don’t compare them with each other. FSR has neither cost. There is no JavaScript engine on the server and a render starts from a tree the build already compiled.
How to read this
These pages are markup-heavy. They loop over data and emit tags, which is most of what a server renderer actually does. It is also where an interpreter walking a lowered tree does less work than a JIT that must first build JavaScript objects to walk. A JIT wins on a component doing real arithmetic 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.