feat: replace react-server-dom-webpack with @lazarv/rsc#390
Conversation
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
react-server-docs | 9e4f76d | Apr 14 2026, 11:24 AM |
⚡ Flight Protocol BenchmarkCommit: Serialization (
|
| 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 Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #390 +/- ##
=======================================
Coverage ? 82.90%
=======================================
Files ? 2
Lines ? 3071
Branches ? 986
=======================================
Hits ? 2546
Misses ? 525
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:
|
⚡ 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. |
Summary
This branch closes a long-standing chapter in the
@lazarv/react-serverruntime: the dependency onreact-server-dom-webpackis gone. In its place, the runtime now uses@lazarv/rsc— a standalone, bundler-agnostic implementation of React'sFlight 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-webpackwas 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 theglobal 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-webpackfollow 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-serverisunchanged. What does change is everything underneath:
Performance is meaningfully better. Benchmarks against the previous
react-server-dom-webpackpath show roughly 2–6× higher serialization throughput, 1.2–11× higher deserialization throughput, and 2–6× roundtrip gains across realisticpayload 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 onNode.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-webpackis removed frompackages/react-server's dependencies. The Webpack module-alias logic, the__webpack_require__interception in the loader, and the chunk-rewriting code in thebuild 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/rscexposes —moduleResolveron the server andmoduleLoaderon the client. Where the old runtime spent its time pretending to be Webpack, the new runtime simplyimplements these two interfaces against its existing Vite-based module system. A small adapter wraps the existing client reference Proxy into the resolver shape
@lazarv/rscexpects, and a thin compatibility wrapper arounddecodeReplyletsolder 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, explicitrequireModulethat the runtime owns. The change is largein line count but conceptually narrow: most of it is removing scaffolding that no longer needs to exist.
The
@lazarv/rscpackage 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 thenablecontract 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 inpackages/rsc/__bench__/, with a CI workflow that runs them on every change. Future regressions willbe 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/rscREADME 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
decodeReplykeeps older internal callers working through the transition. If something does go wrong, rollback is a single-package revert plus restoring thereact-server-dom-webpackdependency inpackages/react-server/package.json; nothing about the runtime's external contract has changed.