Skip to content

feat: replace react-server-dom-webpack with @lazarv/rsc#390

Merged
lazarv merged 17 commits into
mainfrom
fix/rsc-performance
Apr 14, 2026
Merged

feat: replace react-server-dom-webpack with @lazarv/rsc#390
lazarv merged 17 commits into
mainfrom
fix/rsc-performance

Conversation

@lazarv

@lazarv lazarv commented Apr 14, 2026

Copy link
Copy Markdown
Owner

Summary

This branch closes a long-standing chapter in the @lazarv/react-server runtime: the dependency on react-server-dom-webpack is gone. In its place, the runtime now uses @lazarv/rsc — a standalone, bundler-agnostic implementation of React's
Flight protocol that has been incubating in this monorepo. The result is a faster, smaller, more honest runtime: one serialization layer, one code path, one set of primitives that work everywhere the Web Platform does.

Why this matters

react-server-dom-webpack was never a natural fit for this project. It was designed to live inside a Webpack build, where module IDs are integers, manifests are emitted by a Webpack plugin, and __webpack_require__ is a real function on the
global object. None of that exists in a Vite/Rolldown world, so the runtime has spent years synthesizing a fake Webpack environment around every render — fabricating manifests, intercepting globals, rewriting chunk identifiers — just to keep
the upstream package happy. That shim layer was load-bearing, and it leaked: dev and prod bundles of react-server-dom-webpack follow noticeably different code paths, and bugs that reproduced in one frequently disappeared in the other.
Debugging RSC issues meant debugging the impedance mismatch first, the actual problem second.

The deeper cost was strategic. As long as the runtime depended on a Webpack-coupled package, the project's claim of being a true open RSC runtime — bundler-agnostic, framework-agnostic, vendor-neutral — had an asterisk on it. Removing that
dependency removes the asterisk.

What this changes for users

Behaviorally, nothing should change. The wire format is byte-compatible with what React's client expects, server actions and progressive-enhancement form submissions work as before, and the public API surface of @lazarv/react-server is
unchanged. What does change is everything underneath:

  • Performance is meaningfully better. Benchmarks against the previous react-server-dom-webpack path show roughly 2–6× higher serialization throughput, 1.2–11× higher deserialization throughput, and 2–6× roundtrip gains across realistic
    payload shapes. The largest wins show up on wide component trees and streaming-heavy workloads, where the old per-call adapter overhead was most visible. SSR streaming hot paths were retuned along the way, removing redundant encode/decode
    passes and a long-standing payload-duplication issue in the RSC stream.

  • Dev and prod now share one code path. The old runtime effectively shipped two RSC implementations — one for dev, one for prod — and absorbed whatever divergences existed between them. The new layer has a single implementation. An entire
    class of "works in dev, breaks in build" bugs is structurally eliminated.

  • The runtime is portable in a way it wasn't before. Because the new layer is built on Web Platform APIs only — ReadableStream, WritableStream, TextEncoder, FormData, Blob, URL — the same serialization code runs identically on
    Node.js, Bun, Deno, Cloudflare Workers, and in the browser. There are no platform-specific entry points, no conditional imports, and no Node-only fallbacks hiding inside the hot path.

  • The package surface shrinks. react-server-dom-webpack is removed from packages/react-server's dependencies. The Webpack module-alias logic, the __webpack_require__ interception in the loader, and the chunk-rewriting code in the
    build pipeline all go with it. Less to install, less to load, less to reason about.

How the migration was done

The rewrite is structured around two abstract interfaces that @lazarv/rsc exposes — moduleResolver on the server and moduleLoader on the client. Where the old runtime spent its time pretending to be Webpack, the new runtime simply
implements these two interfaces against its existing Vite-based module system. A small adapter wraps the existing client reference Proxy into the resolver shape @lazarv/rsc expects, and a thin compatibility wrapper around decodeReply lets
older internal callers keep passing a manifest as their second argument until they're cleaned up. Server action loading moves from globalThis.__webpack_require__ to a small, explicit requireModule that the runtime owns. The change is large
in line count but conceptually narrow: most of it is removing scaffolding that no longer needs to exist.

The @lazarv/rsc package itself was finished off in the same branch — Flight protocol coverage now extends to typed arrays, Blobs, ReadableStreams, async iterables, temporary references, bound server actions, and the synchronous thenable
contract that React's use() hook depends on. Cross-compatibility tests verify byte-level parity with React's reference encoder for every supported type.

A benchmark suite was added so this isn't a one-time claim. Vitest bench configs, representative fixtures, and webpack-* baselines live in packages/rsc/__bench__/, with a CI workflow that runs them on every change. Future regressions will
be caught the same way functional regressions are.

Documentation

The design rationale is now first-class material. A new "Bundler-Agnostic RSC Serialization" page in the features section explains the problem, the constraint, the decision, and the tradeoffs in the same format as the rest of the
design-decisions guide. The existing design-decisions, micro-frontends, and React integration pages have been updated (English and Japanese) so they no longer reference the removed dependency. The @lazarv/rsc README has been reworded to describe the package on its own terms rather than as a "not-Webpack" alternative.

Risk and rollback

The blast radius is real — every render and every server action flows through the swapped layer — but the change is well-bounded. The wire format is what React's client expects, the cross-compat tests assert that explicitly, and the
compatibility shim around decodeReply keeps older internal callers working through the transition. If something does go wrong, rollback is a single-package revert plus restoring the react-server-dom-webpack dependency in
packages/react-server/package.json; nothing about the runtime's external contract has changed.

@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 9e4f76d Apr 14 2026, 11:24 AM

@github-actions

github-actions Bot commented Apr 14, 2026

Copy link
Copy Markdown

⚡ Flight Protocol Benchmark

Commit: 2a1a3d6

Serialization (renderToReadableStream)

Scenario @lazarv/rsc webpack vs webpack
react: minimal element 235.9K 29.6K 🟢 +697.7%
react: shallow wide (1000) 2.1K 343 🟢 +521.4%
react: deep nested (100) 17.5K 6.0K 🟢 +190.9%
react: product list (50) 5.9K 1.9K 🟢 +204.3%
react: large table (500x10) 278 93 🟢 +199.2%
data: primitives 181.0K 38.8K 🟢 +366.2%
data: large string (100KB) 6.9K 6.8K ⚪ +1.0%
data: nested objects (20) 59.6K 25.9K 🟢 +130.3%
data: large array (10K) 118 112 🟢 +5.2%
data: Map & Set 10.9K 5.7K 🟢 +90.2%
data: Date/BigInt/Symbol 172.1K 36.7K 🟢 +369.3%
data: typed arrays 35.9K 13.0K 🟢 +176.8%
data: mixed payload 8.4K 4.1K 🟢 +105.9%

Prerender (prerender)

Scenario @lazarv/rsc ops/s mean
react: minimal element 250.6K 4.0 µs
react: shallow wide (1000) 2.0K 510.0 µs
react: deep nested (100) 16.2K 61.7 µs
react: product list (50) 5.7K 175.7 µs
react: large table (500x10) 274 3.65 ms
data: primitives 198.1K 5.0 µs
data: large string (100KB) 690 1.45 ms
data: nested objects (20) 58.7K 17.0 µs
data: large array (10K) 117 8.55 ms
data: Map & Set 11.0K 90.7 µs
data: Date/BigInt/Symbol 187.5K 5.3 µs
data: typed arrays 657 1.52 ms
data: mixed payload 7.7K 130.0 µs

Deserialization (createFromReadableStream)

Scenario @lazarv/rsc webpack vs webpack
react: minimal element 170.7K 139.8K 🟢 +22.1%
react: shallow wide (1000) 21.2K 2.0K 🟢 +980.0%
react: deep nested (100) 100.5K 19.4K 🟢 +416.9%
react: product list (50) 52.6K 14.5K 🟢 +261.7%
react: large table (500x10) 4.3K 2.0K 🟢 +111.5%
data: primitives 142.7K 130.2K 🟢 +9.7%
data: large string (100KB) 39.4K 33.8K 🟢 +16.4%
data: nested objects (20) 83.2K 69.9K 🟢 +19.0%
data: large array (10K) 289 260 🟢 +11.3%
data: Map & Set 16.3K 14.5K 🟢 +12.4%
data: Date/BigInt/Symbol 138.8K 110.6K 🟢 +25.6%
data: typed arrays 60.6K 41.0K 🟢 +47.7%
data: mixed payload 25.9K 14.8K 🟢 +74.9%

Roundtrip (serialize + deserialize)

Scenario @lazarv/rsc webpack vs webpack
react: minimal element 85.0K 21.8K 🟢 +289.9%
react: shallow wide (1000) 1.8K 291 🟢 +519.6%
react: deep nested (100) 14.5K 4.1K 🟢 +257.3%
react: product list (50) 5.3K 1.6K 🟢 +228.2%
react: large table (500x10) 271 91 🟢 +199.6%
data: primitives 79.0K 28.8K 🟢 +174.1%
data: large string (100KB) 6.7K 6.9K 🔴 -2.7%
data: nested objects (20) 34.8K 18.4K 🟢 +89.2%
data: large array (10K) 83 76 🟢 +9.6%
data: Map & Set 6.2K 4.0K 🟢 +57.5%
data: Date/BigInt/Symbol 74.8K 23.4K 🟢 +219.6%
data: typed arrays 26.1K 9.8K 🟢 +165.4%
data: mixed payload 6.0K 2.8K 🟢 +114.8%
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.

@codecov-commenter

codecov-commenter commented Apr 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 67.18615% with 379 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@464b5a5). Learn more about missing BASE report.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/rsc/client/shared.mjs 72.89% 196 Missing ⚠️
packages/rsc/server/shared.mjs 57.63% 183 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #390   +/-   ##
=======================================
  Coverage        ?   82.90%           
=======================================
  Files           ?        2           
  Lines           ?     3071           
  Branches        ?      986           
=======================================
  Hits            ?     2546           
  Misses          ?      525           
  Partials        ?        0           
Flag Coverage Δ
rsc 82.90% <67.18%> (?)

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

github-actions Bot commented Apr 14, 2026

Copy link
Copy Markdown

⚡ Benchmark Results

PR 723f470 main 464b5a5
Config 50 connections, 10s/test 50 connections, 10s/test
Benchmark Req/s vs main Avg Latency vs main P99 Latency Throughput
minimal 1450 🟢 +20.6% 33.99 ms 🟢 -17.2% 76 ms 1.0 MB/s
small 1419 🟢 +8.3% 34.69 ms 🟢 -7.8% 60 ms 1.5 MB/s
medium 416 🟢 +25.6% 119 ms 🟢 -19.8% 159 ms 6.1 MB/s
large 50 🟢 +55.3% 956.12 ms 🟢 -36.5% 1880 ms 5.0 MB/s
deep 961 🟢 +13.3% 51.39 ms 🟢 -11.8% 83 ms 3.3 MB/s
wide 72 🟢 +82.7% 671.08 ms 🟢 -42.9% 1053 ms 3.9 MB/s
cached 3586 🟢 +6.7% 13.35 ms 🟢 -7.3% 27 ms 52.7 MB/s
client-min 538 🟢 +22.2% 92.05 ms 🟢 -18.0% 157 ms 2.3 MB/s
client-small 578 🟢 +28.4% 85.78 ms 🟢 -22.2% 131 ms 2.7 MB/s
client-med 412 🟢 +18.5% 120.32 ms 🟢 -15.6% 179 ms 7.6 MB/s
client-large 89 🟢 +8.5% 541.02 ms 🟢 -8.2% 915 ms 9.4 MB/s
client-deep 527 🟢 +22.7% 94.24 ms 🟢 -18.3% 140 ms 3.8 MB/s
client-wide 156 🟢 +11.7% 311.67 ms 🟢 -10.9% 505 ms 9.1 MB/s
rsc-client-large 1265 38.94 ms 58 ms 3.3 MB/s
rsc-client-wide 1259 39.12 ms 60 ms 3.3 MB/s
static-json 10269 🟢 +54.3% 4.6 ms 🟢 -34.7% 14 ms 4.3 MB/s
static-js 10236 🟢 +57.7% 4.6 ms 🟢 -36.6% 13 ms 6.1 MB/s
404-miss 5625 🟢 +16.8% 8.25 ms 🟢 -15.8% 19 ms 0.7 MB/s
hybrid-min 556 🟢 +25.2% 89.14 ms 🟢 -19.8% 144 ms 2.7 MB/s
hybrid-small 528 🟢 +22.4% 93.81 ms 🟢 -18.0% 141 ms 3.1 MB/s
hybrid-medium 267 🟢 +36.9% 183.89 ms 🟢 -26.7% 264 ms 11.4 MB/s
hybrid-large 45 🟢 +88.0% 1059.33 ms 🟢 -42.8% 1725 ms 14.6 MB/s
hybrid-deep 421 🟢 +25.8% 117.55 ms 🟢 -20.2% 166 ms 5.8 MB/s
hybrid-wide 65 🟢 +108.4% 740.5 ms 🟢 -47.5% 1271 ms 12.7 MB/s
hybrid-cached 2980 🟢 +5.7% 16.23 ms 🟢 -5.7% 31 ms 126.6 MB/s
hybrid-client-min 577 🟢 +21.1% 85.76 ms 🟢 -17.2% 128 ms 2.6 MB/s
hybrid-client-small 579 🟢 +20.8% 85.64 ms 🟢 -17.1% 129 ms 2.8 MB/s
hybrid-client-medium 416 🟢 +15.3% 118.49 ms 🟢 -13.8% 172 ms 7.7 MB/s
hybrid-client-large 90 🟢 +6.8% 539.41 ms 🟢 -6.2% 989 ms 9.4 MB/s
hybrid-client-deep 519 🟢 +21.9% 95.74 ms 🟢 -17.6% 166 ms 3.8 MB/s
hybrid-client-wide 150 🟢 +5.1% 329.13 ms 🟢 -3.7% 582 ms 8.7 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 6fa0c2d into main Apr 14, 2026
70 of 71 checks passed
@lazarv lazarv deleted the fix/rsc-performance branch April 14, 2026 12:37
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