feat: replace react-server-dom-webpack with @lazarv/rsc (#390)
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-webpackpath 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-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 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.