react-server675fbba4
react-servercommit6fa0c2d125e3

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-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.

Author
Viktor Lázár <lazarv1982@gmail.com>
Date
Commit
6fa0c2d125e3d1ad0508c41480222716dd5810ea
84 files changed+6092 -1790