Conversation
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
react-server-docs | c713a0f | Apr 30 2026, 10:51 PM |
⚡ Flight Protocol BenchmarkCommit: Serialization (
|
| Scenario | @lazarv/rsc | webpack | vs webpack |
|---|---|---|---|
| react: minimal element | 228.6K | 29.2K | 🟢 +682.2% |
| react: shallow wide (1000) | 2.1K | 373 | 🟢 +471.5% |
| react: deep nested (100) | 17.2K | 6.0K | 🟢 +188.3% |
| react: product list (50) | 6.1K | 1.9K | 🟢 +219.1% |
| react: large table (500x10) | 280 | 91 | 🟢 +205.9% |
| data: primitives | 180.4K | 33.6K | 🟢 +437.5% |
| data: large string (100KB) | 7.0K | 7.0K | ⚪ -0.8% |
| data: nested objects (20) | 56.5K | 26.3K | 🟢 +114.9% |
| data: large array (10K) | 118 | 105 | 🟢 +11.7% |
| data: Map & Set | 10.8K | 5.9K | 🟢 +81.9% |
| data: Date/BigInt/Symbol | 166.5K | 35.9K | 🟢 +363.7% |
| data: typed arrays | 33.2K | 12.5K | 🟢 +165.8% |
| data: mixed payload | 8.0K | 4.1K | 🟢 +95.1% |
Prerender (prerender)
| Scenario | @lazarv/rsc ops/s | mean |
|---|---|---|
| react: minimal element | 258.8K | 3.9 µs |
| react: shallow wide (1000) | 2.0K | 494.1 µs |
| react: deep nested (100) | 16.2K | 61.9 µs |
| react: product list (50) | 5.6K | 177.7 µs |
| react: large table (500x10) | 272 | 3.68 ms |
| data: primitives | 191.5K | 5.2 µs |
| data: large string (100KB) | 690 | 1.45 ms |
| data: nested objects (20) | 56.6K | 17.7 µs |
| data: large array (10K) | 116 | 8.65 ms |
| data: Map & Set | 11.3K | 88.9 µs |
| data: Date/BigInt/Symbol | 185.8K | 5.4 µs |
| data: typed arrays | 665 | 1.50 ms |
| data: mixed payload | 7.6K | 131.6 µs |
Deserialization (createFromReadableStream)
| Scenario | @lazarv/rsc | webpack | vs webpack |
|---|---|---|---|
| react: minimal element | 167.3K | 137.1K | 🟢 +22.0% |
| react: shallow wide (1000) | 19.9K | 2.0K | 🟢 +916.6% |
| react: deep nested (100) | 101.3K | 18.4K | 🟢 +451.4% |
| react: product list (50) | 52.2K | 14.5K | 🟢 +258.7% |
| react: large table (500x10) | 4.2K | 2.0K | 🟢 +111.3% |
| data: primitives | 138.6K | 129.9K | 🟢 +6.8% |
| data: large string (100KB) | 40.2K | 30.5K | 🟢 +31.6% |
| data: nested objects (20) | 84.3K | 70.2K | 🟢 +20.0% |
| data: large array (10K) | 279 | 246 | 🟢 +13.4% |
| data: Map & Set | 16.8K | 14.4K | 🟢 +16.6% |
| data: Date/BigInt/Symbol | 138.1K | 110.0K | 🟢 +25.6% |
| data: typed arrays | 55.5K | 42.8K | 🟢 +29.6% |
| data: mixed payload | 25.4K | 14.9K | 🟢 +70.5% |
Roundtrip (serialize + deserialize)
| Scenario | @lazarv/rsc | webpack | vs webpack |
|---|---|---|---|
| react: minimal element | 105.5K | 21.8K | 🟢 +385.0% |
| react: shallow wide (1000) | 1.8K | 271 | 🟢 +562.4% |
| react: deep nested (100) | 14.7K | 4.2K | 🟢 +246.9% |
| react: product list (50) | 5.4K | 1.7K | 🟢 +225.4% |
| react: large table (500x10) | 266 | 84 | 🟢 +218.3% |
| data: primitives | 81.9K | 25.4K | 🟢 +222.3% |
| data: large string (100KB) | 5.9K | 7.0K | 🔴 -16.9% |
| data: nested objects (20) | 34.0K | 18.2K | 🟢 +86.8% |
| data: large array (10K) | 78 | 72 | 🟢 +8.4% |
| data: Map & Set | 6.1K | 4.0K | 🟢 +54.1% |
| data: Date/BigInt/Symbol | 71.1K | 22.6K | 🟢 +214.4% |
| data: typed arrays | 25.1K | 11.2K | 🟢 +123.8% |
| data: mixed payload | 5.9K | 3.0K | 🟢 +96.3% |
Legend & methodology
Indicators: 🟢 > 1% faster | 🔴 > 1% slower | ⚪ within noise margin
vs webpack: compares @lazarv/rsc against react-server-dom-webpack within the same run.
vs baseline: compares @lazarv/rsc against the previous main branch run.
Values shown are operations/second (higher is better). Each scenario runs for at least 100 iterations with warmup.
Benchmarks run on GitHub Actions runners (shared infrastructure) — expect ~5% variance between runs. Consistent directional changes across multiple scenarios are more meaningful than any single number.
❌ 1 Tests Failed:
View the top 1 failed test(s) by shortest run time
To view more test analytics, go to the Test Analytics Dashboard |
⚡ Benchmark Results
Legend🟢 > 1% improvement | 🔴 > 1% regression | ⚪ within noise margin Benchmarks run on GitHub Actions runners (shared infrastructure) — expect ~5% variance between runs. Consistent directional changes across multiple routes are more meaningful than any single number. |
The static exporter has been rebuilt around a streaming path source and an opt-in multi-process render coordinator. Before this change, exporting a large site materialised the full path list into an array, then ran every render in
Promise.allon a single thread. At ~24k pages of Shiki-rendered HTML the resident set climbed into the multi-gigabyte range even though only a handful of pages were live at any instant, and runs would OOM long before they finished. Sites with non-trivial server-component work were also bottlenecked on a single RSC thread regardless of available CPU.The new pipeline splits that work in two. A shared streaming layer (
buildPathStream,pMapStream,fanout) consumes path sources lazily —options.exportPathsandconfigRoot.exportcan now be async generators or any AsyncIterable, and the renderer pulls one path per free worker, so peak memory isO(concurrency × chunkSize)regardless of total path count. Layered on top of that, a forked coordinator can spread the render across N child processes, each running its own RSC main thread and SSR worker thread, dispatching paths over IPC. Every artifact (HTML, gz/br sidecars,.postponed.json,.prerender-cache.json) is written to disk inside the child, so output bytes never cross the IPC boundary; the coordinator only ferries small log entries back. The two modes produce an identical artifact set — postpone and prerender-cache work in multi-process mode because each child uses the samesetupStaticRender+emitAllArtifactspath the single-process exporter does.The number of render workers is exposed as a new
--export-concurrencyflag (and anexportConcurrencyfield inreact-server.config.mjs), defaulting to a CPU-scaled value between 2 and 4. Setting it to1collapses to the single-process in-line exporter — cheapest for small sites and useful for debugging. Documentation has been added in both English and Japanese covering the flag itself and the documented "level up" pattern forconfig.exportas anasync function*for cases where the path list is too large to materialise (CMS pagination, large database queries, tens of thousands of file-router slugs).A handful of related bugs surfaced and were fixed alongside the rewrite. The backpressure PR had moved
prerenderInitafter the static handlers inlib/start/create-server.mjs, which broke the PPR resume flow becauselib/handlers/static.mjsmutatesPrerenderStoragewhen it serves a.postponed.jsonsidecar; the middleware is moved back ahead of the static handlers. The static-worker child'sfatal()was racingprocess.exit(1)against an unflushed IPCsend, so worker crashes appeared to the parent as bare exit codes with no underlying error — fixed by waiting on the send callback before exiting. The renderer worker URL instatic-runtime.mjswas incorrect (./render-stream.mjsresolved intolib/build/); it now resolves correctly intolib/start/. The Postponed control signal that React's prerender machinery throws at dynamic boundaries was being printed as a red error stack by the static exporter's logger proxy; it's now filtered by theREACT_SERVER_POSTPONEDdigest. Per-path render failures are once again printed in red on stderr above the summary line — the streaming refactor had silently dropped the error log while keeping the count. The compression default is restored to the documentedfalse(a previous attempt to fix "compressed sizes not shown" in multi-process had inadvertently flipped the default on); the real fix lives in the IPC plumbing instead. And the prerender-disabled check inemitHtmlnow gates the entire postpone/prerender-cache machinery (the callback wiring, the cacheSetallocation, the sidecar emission) on a single computedprerenderEnabled, derived from both the per-path and config-level prerender state — previously a per-pathprerender: falseflowed into render but still installed theonPostponedcallback.Tests cover the new pipeline at three layers. Unit tests exercise each streaming primitive in isolation —
pMapStreamfor bounded concurrency and lazy pulls,toPathStreamfor input-shape normalisation,buildPathStreamfor the generator/array branch split (including a passthrough-then-append pattern that mirrors the docs and a fail-fast error-propagation case),validatedPathStreamfor descriptor validation, andfanoutfor backpressured one-chunk-in-flight delivery. A second describe asserts the load-bearing structural invariant —pulled - completed <= concurrency— at a modest N where the assertion is N-independent but cheap. The end-to-end at-scale coverage lives in a new fixture spec undertest-build-start:test/fixtures/static-export-many/is a two-file fixture (a single-component entry plus areact-server.config.mjswhoseexportis anasync function*yieldingSTATIC_EXPORT_MANY_COUNTpaths, defaulting to 100k), and the spec runs the full build + serve round-trip and spot-checks four sample paths. The harness'svitestSetup.mjsnow derivesoptions.exportfrom a serialisableinitialConfig.exportflag so a spec can opt into static export without touching internal build options; the actual generator function still lives on disk because functions can't cross the JSON boundary into the build worker.