Conversation
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
react-server-docs | 3785c21 | May 08 2026, 09:00 AM |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #420 +/- ##
=======================================
Coverage ? 91.99%
=======================================
Files ? 3
Lines ? 3596
Branches ? 1176
=======================================
Hits ? 3308
Misses ? 288
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
⚡ Flight Protocol BenchmarkCommit: Serialization (
|
| Scenario | @lazarv/rsc | webpack | vs webpack |
|---|---|---|---|
| react: minimal element | 224.5K | 26.1K | 🟢 +758.6% |
| react: shallow wide (1000) | 2.2K | 352 | 🟢 +521.8% |
| react: deep nested (100) | 16.9K | 5.8K | 🟢 +190.7% |
| react: product list (50) | 6.0K | 1.9K | 🟢 +207.1% |
| react: large table (500x10) | 274 | 94 | 🟢 +190.4% |
| data: primitives | 178.2K | 40.2K | 🟢 +343.8% |
| data: large string (100KB) | 7.4K | 6.7K | 🟢 +10.5% |
| data: nested objects (20) | 58.5K | 26.0K | 🟢 +124.9% |
| data: large array (10K) | 116 | 113 | 🟢 +2.4% |
| data: Map & Set | 10.8K | 5.8K | 🟢 +85.2% |
| data: Date/BigInt/Symbol | 164.3K | 35.9K | 🟢 +357.8% |
| data: typed arrays | 32.5K | 12.6K | 🟢 +157.6% |
| data: mixed payload | 8.3K | 4.1K | 🟢 +103.7% |
Prerender (prerender)
| Scenario | @lazarv/rsc ops/s | mean |
|---|---|---|
| react: minimal element | 241.4K | 4.1 µs |
| react: shallow wide (1000) | 2.0K | 504.7 µs |
| react: deep nested (100) | 16.0K | 62.7 µs |
| react: product list (50) | 5.7K | 175.5 µs |
| react: large table (500x10) | 273 | 3.66 ms |
| data: primitives | 191.3K | 5.2 µs |
| data: large string (100KB) | 690 | 1.45 ms |
| data: nested objects (20) | 58.8K | 17.0 µs |
| data: large array (10K) | 118 | 8.51 ms |
| data: Map & Set | 11.2K | 89.6 µs |
| data: Date/BigInt/Symbol | 182.5K | 5.5 µs |
| data: typed arrays | 669 | 1.49 ms |
| data: mixed payload | 7.6K | 131.8 µs |
Deserialization (createFromReadableStream)
| Scenario | @lazarv/rsc | webpack | vs webpack |
|---|---|---|---|
| react: minimal element | 168.5K | 137.0K | 🟢 +23.0% |
| react: shallow wide (1000) | 22.5K | 2.0K | 🟢 +1045.9% |
| react: deep nested (100) | 101.5K | 19.3K | 🟢 +426.1% |
| react: product list (50) | 52.7K | 14.7K | 🟢 +258.6% |
| react: large table (500x10) | 4.1K | 2.2K | 🟢 +90.0% |
| data: primitives | 140.8K | 126.2K | 🟢 +11.6% |
| data: large string (100KB) | 42.6K | 33.3K | 🟢 +27.7% |
| data: nested objects (20) | 83.3K | 68.9K | 🟢 +20.8% |
| data: large array (10K) | 289 | 232 | 🟢 +24.6% |
| data: Map & Set | 16.7K | 14.6K | 🟢 +14.3% |
| data: Date/BigInt/Symbol | 135.8K | 106.6K | 🟢 +27.4% |
| data: typed arrays | 52.4K | 43.4K | 🟢 +20.9% |
| data: mixed payload | 25.7K | 14.7K | 🟢 +75.5% |
Roundtrip (serialize + deserialize)
| Scenario | @lazarv/rsc | webpack | vs webpack |
|---|---|---|---|
| react: minimal element | 102.8K | 21.8K | 🟢 +371.5% |
| react: shallow wide (1000) | 1.7K | 295 | 🟢 +484.7% |
| react: deep nested (100) | 14.6K | 4.2K | 🟢 +248.8% |
| react: product list (50) | 5.3K | 1.7K | 🟢 +223.1% |
| react: large table (500x10) | 267 | 94 | 🟢 +185.1% |
| data: primitives | 81.1K | 28.5K | 🟢 +184.8% |
| data: large string (100KB) | 6.6K | 6.6K | ⚪ +0.1% |
| data: nested objects (20) | 33.9K | 18.1K | 🟢 +87.1% |
| data: large array (10K) | 82 | 77 | 🟢 +6.2% |
| data: Map & Set | 6.2K | 3.9K | 🟢 +59.5% |
| data: Date/BigInt/Symbol | 71.7K | 22.5K | 🟢 +218.7% |
| data: typed arrays | 23.8K | 11.2K | 🟢 +111.7% |
| data: mixed payload | 6.0K | 2.9K | 🟢 +105.0% |
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. |
Live Components, until now, hardcoded socket.io as the only transport. Every project shipping
@lazarv/react-serverpaid the socket.io footprint whether or not it used"use live"directives, and there was no way to opt into SSE for cache-friendly fan-out or a leaner ws transport when full duplex was overkill.This PR makes the transport pluggable. A live component declares its transport in the directive itself —
"use live; transport=ws","use live; transport=sse", or"use live; transport=socketio"(the previous default). The runtime owns aLIVE_TRANSPORTregistry; each transport registers an attach-on-demand factory, and the registry exposes anensure(name)helper that lazy-attaches when a directive references a transport that hasn't been wired yet. The practical consequence is that a project with no live components imports zero transports, including zero socket.io.Two cross-cutting fixes shook out of getting this working end-to-end. The
transport.channel(outlet)call site assumed the channel handle was always async — true for socketio and ws, false for SSE, which returns synchronously. Wrapping the result inPromise.resolve(...)keeps the call site uniform without forcing every transport author to wrap their own returns. Separately, the runtime'sEXEC_OPTIONSregistry was leaking the livehttp.Serverinstance into every downstream consumer that crossed a structured-clone boundary —worker_threads.workerData, the loader thread'smodule.register()datapayload, and any user-landuseWorkerproxy. StrippinghttpServeronce at the source (whereEXEC_OPTIONSis written) replaces three scattered local strips and prevents a future clone-crossing consumer from reintroducing the sameDataCloneError: function() { ... } could not be clonedregression.While exercising the transport switch through the remote example, the SSR dispatcher in
ssr-handler.mjsturned out to be misrouting.remote.x-componentrequests. The check used a substring match againsthttpContext.url.pathname, butcreateRenderContextrewrites that pathname (strips the suffix and@<outlet>.prefix) before the dispatcher runs, so the substring match always returned false and remote requests fell into the render-ssr path that produces HTML instead of Flight. The fix readsrenderContext.flags?.isRemotedirectly — set canonically at the regex match site — and routes those requests throughrender-rsc.jsxthe same way server-function POSTs already are. As defense-in-depth, the build now emitsrender-action.mjsunconditionally rather than gating onisClientRootBuild; a client-root build that hosts a remote-component endpoint needs that entry, and the previous gate was the bug.Test coverage for the monitor and remote examples lands in both dev and build-start modes. The monitor test verifies live-channel updates flow through the new transport. The remote test exercises a host plus seven aux origins — one per remote-component shape (server-function, static, streaming, live, navigation, form, context) — and confirms the host page renders content from every origin, including the IPv6 (
[::1]:3001) entry and shadow-DOM-isolated payloads. A newtest/server.aux.mjsrunner spawns each aux as its own forked process matching the example's prod/dev mode, with a readiness probe that gates host startup on actual remote-component reachability rather than just socket-listening state — closing the race where the host build pre-fetches a not-yet-warm aux's introspection endpoint and bakes a partial payload into the bundle.