Conversation
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
react-server-docs | 62fe116 | Apr 29 2026, 12:07 PM |
⚡ Flight Protocol BenchmarkCommit: Serialization (
|
| Scenario | @lazarv/rsc | webpack | vs webpack |
|---|---|---|---|
| react: minimal element | 218.1K | 27.5K | 🟢 +691.8% |
| react: shallow wide (1000) | 2.0K | 332 | 🟢 +513.9% |
| react: deep nested (100) | 17.2K | 5.5K | 🟢 +214.1% |
| react: product list (50) | 6.1K | 2.0K | 🟢 +209.6% |
| react: large table (500x10) | 280 | 88 | 🟢 +219.7% |
| data: primitives | 178.1K | 35.0K | 🟢 +408.4% |
| data: large string (100KB) | 7.2K | 7.2K | ⚪ -0.6% |
| data: nested objects (20) | 57.9K | 25.8K | 🟢 +124.7% |
| data: large array (10K) | 115 | 110 | 🟢 +4.8% |
| data: Map & Set | 10.9K | 5.6K | 🟢 +96.6% |
| data: Date/BigInt/Symbol | 162.6K | 33.6K | 🟢 +384.4% |
| data: typed arrays | 32.6K | 13.2K | 🟢 +146.4% |
| data: mixed payload | 8.2K | 4.1K | 🟢 +100.0% |
Prerender (prerender)
| Scenario | @lazarv/rsc ops/s | mean |
|---|---|---|
| react: minimal element | 234.5K | 4.3 µs |
| react: shallow wide (1000) | 2.0K | 504.8 µs |
| react: deep nested (100) | 15.8K | 63.2 µs |
| react: product list (50) | 5.5K | 182.9 µs |
| react: large table (500x10) | 265 | 3.77 ms |
| data: primitives | 188.8K | 5.3 µs |
| data: large string (100KB) | 682 | 1.47 ms |
| data: nested objects (20) | 56.5K | 17.7 µs |
| data: large array (10K) | 112 | 8.92 ms |
| data: Map & Set | 11.0K | 91.2 µs |
| data: Date/BigInt/Symbol | 180.6K | 5.5 µs |
| data: typed arrays | 675 | 1.48 ms |
| data: mixed payload | 7.5K | 134.2 µs |
Deserialization (createFromReadableStream)
| Scenario | @lazarv/rsc | webpack | vs webpack |
|---|---|---|---|
| react: minimal element | 168.3K | 129.9K | 🟢 +29.5% |
| react: shallow wide (1000) | 22.7K | 1.9K | 🟢 +1114.7% |
| react: deep nested (100) | 101.2K | 19.0K | 🟢 +433.8% |
| react: product list (50) | 52.6K | 14.3K | 🟢 +268.9% |
| react: large table (500x10) | 4.0K | 2.1K | 🟢 +87.5% |
| data: primitives | 138.3K | 125.5K | 🟢 +10.2% |
| data: large string (100KB) | 35.6K | 34.2K | 🟢 +4.2% |
| data: nested objects (20) | 83.1K | 71.2K | 🟢 +16.6% |
| data: large array (10K) | 280 | 249 | 🟢 +12.7% |
| data: Map & Set | 16.5K | 14.4K | 🟢 +14.3% |
| data: Date/BigInt/Symbol | 134.5K | 109.0K | 🟢 +23.4% |
| data: typed arrays | 54.8K | 43.2K | 🟢 +26.7% |
| data: mixed payload | 25.8K | 14.0K | 🟢 +84.8% |
Roundtrip (serialize + deserialize)
| Scenario | @lazarv/rsc | webpack | vs webpack |
|---|---|---|---|
| react: minimal element | 102.1K | 20.7K | 🟢 +392.6% |
| react: shallow wide (1000) | 1.8K | 266 | 🟢 +564.1% |
| react: deep nested (100) | 14.5K | 4.3K | 🟢 +240.1% |
| react: product list (50) | 5.4K | 1.7K | 🟢 +222.4% |
| react: large table (500x10) | 257 | 85 | 🟢 +203.8% |
| data: primitives | 79.4K | 29.8K | 🟢 +166.8% |
| data: large string (100KB) | 6.1K | 6.6K | 🔴 -7.7% |
| data: nested objects (20) | 33.7K | 17.9K | 🟢 +88.2% |
| data: large array (10K) | 82 | 76 | 🟢 +7.1% |
| data: Map & Set | 6.2K | 3.8K | 🟢 +62.2% |
| data: Date/BigInt/Symbol | 70.7K | 20.8K | 🟢 +239.9% |
| data: typed arrays | 26.0K | 11.1K | 🟢 +134.3% |
| data: mixed payload | 5.9K | 3.1K | 🟢 +94.5% |
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.
⚡ 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. |
❌ 1 Tests Failed:
View the top 3 failed test(s) by shortest run time
To view more test analytics, go to the Test Analytics Dashboard |
Summary
Introduces a new react-server directive,
"use client; no-ssr", for client components whose dependency graph only makes sense in the browser. A plain"use client"module still renders during SSR — its imports get bundled and evaluated on the server even though only the interactivity ships to the client. For components that pull in WebGL contexts, charting libraries, code editors, or anything else that toucheswindowat module scope, that means hundreds of KiB of JavaScript landing in the SSR/edge worker bundle to render an empty wrapper.The new directive replaces the module with a null-rendering stub in the SSR build (no imports of the implementation graph at all) and emits a wrapper in the client build that imports the original through a virtual id and renders it inside
<ClientOnly>. The two halves match up at hydration: SSR sends nothing, the client renders nothing on the first pass, then transitions to the real component afteruseEffect. No hydration mismatch, no heavy code in the worker.How it's wired
The transform lives in
lib/plugins/use-client.mjs. The RSC build branch is unchanged (registerClientReference). The SSR branch, when it sees a no-ssr module, walks the AST for the default and named exports and emits stubs that all returnnull. The client branch, gated onenforce: "pre"in the build mode only, emits a"use client"wrapper module that imports the original viavirtual:no-ssr-original:<absolute-path>. The correspondingresolveId/loadpair on the same plugin returns the file content verbatim; an early-return at the top of the transform handler skips the virtual id so the wrapper logic doesn't recurse on itself.Directive matching is centralised in a new
lib/utils/directives.mjshelper,parseClientDirective(directives). The grammar is permissive — it splits on;and trims segments — so"use client","use client;no-ssr","use client; no-ssr","use client; no-ssr", and"use client ; no-ssr"all parse to the same{ isClient, isNoSSR }shape. Every directive-aware site in the runtime now goes through the parser instead of enumerating string variants:use-client.mjs,use-server.mjs,file-router/plugin.mjs(isClientPageSourceandisClientSource), andbuild/server.mjsroot detection.use-directive-inline.mjswas extended soskipIfModuleDirectivecan be a predicate, anduse-client-inline.mjsswitched to that form to handle whitespace variants without enumeration. Substring fast-paths inlib/utils/module.mjsandlib/build/server.mjshad their closing quotes dropped ('"use client'instead of'"use client"') so any modifier form is captured by the cheap pre-filter.Demo: docs site
docs/src/components/ReactViteScene.jsxwas the canary. It previously routed Three.js through dynamicimport()insideuseEffectto keep ~520 KiB of WebGL out of the worker bundle. With the new directive in place, the dynamic-import workaround is gone and the imports are static again. Verified against the produced bundle: the Cloudflare worker output (.cloudflare/worker/.react-server/server/edge.mjsand the surrounding chunks) has zero references toWebGLRenderer,MeshPhysicalMaterial,EffectComposer,UnrealBloomPass,PMREMGenerator,ACESFilmicToneMapping, or any other Three.js identifier — the SSR-sideReactViteScenechunk is the 116-byte null stub. The client bundle keeps the full 566 KiB Three.js chunk and only loads it on pages that actually mount the scene.