Skip to content

feat: live component transport types#420

Merged
lazarv merged 8 commits intomainfrom
feat/live-component-transport
May 8, 2026
Merged

feat: live component transport types#420
lazarv merged 8 commits intomainfrom
feat/live-component-transport

Conversation

@lazarv
Copy link
Copy Markdown
Owner

@lazarv lazarv commented May 8, 2026

Live Components, until now, hardcoded socket.io as the only transport. Every project shipping @lazarv/react-server paid 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 a LIVE_TRANSPORT registry; each transport registers an attach-on-demand factory, and the registry exposes an ensure(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 in Promise.resolve(...) keeps the call site uniform without forcing every transport author to wrap their own returns. Separately, the runtime's EXEC_OPTIONS registry was leaking the live http.Server instance into every downstream consumer that crossed a structured-clone boundary — worker_threads.workerData, the loader thread's module.register() data payload, and any user-land useWorker proxy. Stripping httpServer once at the source (where EXEC_OPTIONS is written) replaces three scattered local strips and prevents a future clone-crossing consumer from reintroducing the same DataCloneError: function() { ... } could not be cloned regression.

While exercising the transport switch through the remote example, the SSR dispatcher in ssr-handler.mjs turned out to be misrouting .remote.x-component requests. The check used a substring match against httpContext.url.pathname, but createRenderContext rewrites 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 reads renderContext.flags?.isRemote directly — set canonically at the regex match site — and routes those requests through render-rsc.jsx the same way server-function POSTs already are. As defense-in-depth, the build now emits render-action.mjs unconditionally rather than gating on isClientRootBuild; 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 new test/server.aux.mjs runner 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.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
react-server-docs 3785c21 May 08 2026, 09:00 AM

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 8, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@6b86f6a). Learn more about missing BASE report.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #420   +/-   ##
=======================================
  Coverage        ?   91.99%           
=======================================
  Files           ?        3           
  Lines           ?     3596           
  Branches        ?     1176           
=======================================
  Hits            ?     3308           
  Misses          ?      288           
  Partials        ?        0           
Flag Coverage Δ
rsc 91.99% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2026

⚡ Flight Protocol Benchmark

Commit: b50e3b3

Serialization (renderToReadableStream)

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.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2026

⚡ Benchmark Results

PR 5ff420d main 88aa2b7
Config 50 connections, 10s/test 50 connections, 10s/test
Benchmark Req/s vs main Avg Latency vs main P99 Latency Throughput
minimal 1340 🟢 +8.7% 36.71 ms 🟢 -8.0% 73 ms 0.9 MB/s
small 1444 🟢 +20.3% 34.01 ms 🟢 -17.0% 59 ms 1.4 MB/s
medium 409 🟢 +6.7% 120.73 ms 🟢 -6.6% 177 ms 6.0 MB/s
large 51 🟢 +10.6% 947.26 ms 🟢 -6.7% 1964 ms 5.2 MB/s
deep 933 🟢 +7.1% 52.91 ms 🟢 -6.5% 84 ms 3.2 MB/s
wide 70 🟢 +3.1% 674.02 ms 🟢 -5.8% 1133 ms 3.8 MB/s
cached 3356 🔴 -2.3% 14.44 ms 🔴 +2.8% 28 ms 49.3 MB/s
client-min 506 🟢 +14.8% 97.8 ms 🟢 -12.7% 163 ms 2.1 MB/s
client-small 530 🟢 +13.8% 93.19 ms 🟢 -12.3% 141 ms 2.4 MB/s
client-med 392 🟢 +11.8% 125.91 ms 🟢 -10.4% 182 ms 7.2 MB/s
client-large 84 🟢 +7.9% 568.2 ms 🟢 -10.1% 960 ms 8.8 MB/s
client-deep 498 🟢 +17.2% 98.84 ms 🟢 -14.5% 158 ms 3.5 MB/s
client-wide 153 🟢 +15.1% 320.57 ms 🟢 -12.9% 570 ms 8.9 MB/s
rsc-client-large 1211 🟢 +13.1% 40.62 ms 🟢 -11.7% 56 ms 3.1 MB/s
rsc-client-wide 1239 🟢 +14.3% 39.71 ms 🟢 -12.6% 61 ms 3.2 MB/s
static-json 9476 🟢 +28.8% 4.78 ms 🟢 -24.7% 15 ms 3.9 MB/s
static-js 9347 🟢 +34.3% 4.75 ms 🟢 -28.5% 16 ms 11.7 MB/s
404-miss 5156 🟢 +6.9% 9.08 ms 🟢 -6.5% 21 ms 0.6 MB/s
hybrid-min 516 🟢 +17.2% 95.66 ms 🟢 -14.9% 147 ms 2.4 MB/s
hybrid-small 488 🟢 +14.7% 101.36 ms 🟢 -12.7% 152 ms 2.8 MB/s
hybrid-medium 249 🟢 +12.9% 196.39 ms 🟢 -11.5% 270 ms 10.6 MB/s
hybrid-large 40 🟢 +2.2% 1163.52 ms 🟢 -4.8% 2018 ms 12.7 MB/s
hybrid-deep 400 🟢 +16.1% 123.42 ms 🟢 -14.2% 172 ms 5.5 MB/s
hybrid-wide 65 🟢 +13.7% 758.85 ms 🟢 -8.2% 1285 ms 12.8 MB/s
hybrid-cached 2904 🔴 -1.6% 16.65 ms 🔴 +1.3% 31 ms 123.3 MB/s
hybrid-client-min 555 🟢 +22.8% 89.21 ms 🟢 -18.5% 140 ms 2.4 MB/s
hybrid-client-small 559 🟢 +23.4% 88.63 ms 🟢 -18.8% 133 ms 2.6 MB/s
hybrid-client-medium 402 🟢 +17.6% 122.95 ms 🟢 -15.0% 182 ms 7.4 MB/s
hybrid-client-large 90 🟢 +18.3% 548.24 ms 🟢 -13.6% 1109 ms 9.5 MB/s
hybrid-client-deep 511 🟢 +23.5% 96.65 ms 🟢 -18.7% 149 ms 3.7 MB/s
hybrid-client-wide 151 🟢 +12.1% 325.94 ms 🟢 -11.5% 616 ms 8.8 MB/s
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.

@lazarv lazarv merged commit 96c56e7 into main May 8, 2026
93 of 98 checks passed
@lazarv lazarv deleted the feat/live-component-transport branch May 8, 2026 15:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants