Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,76 @@ jobs:
echo "Creating new comment"
gh pr comment "$PR_NUMBER" --body-file examples/benchmark/comment.md
fi

flight-benchmark:
name: Flight protocol benchmark
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4

- uses: ./.github/workflows/actions/common-setup

- name: Run flight benchmarks
working-directory: packages/rsc
run: pnpm bench:json
env:
NODE_OPTIONS: --max-old-space-size=6144

- name: Process results & generate report
working-directory: packages/rsc
run: node __bench__/report.mjs --current bench-raw.json --output bench-results.json --markdown comment.md --commit ${{ github.sha }}

- name: Upload benchmark results
uses: actions/upload-artifact@v4
with:
name: flight-bench-${{ github.sha }}
path: packages/rsc/bench-results.json
retention-days: 90

# ── Main branch: save baseline to cache ──────────────────────────────
- name: Save baseline to cache
if: github.ref == 'refs/heads/main'
uses: actions/cache/save@v4
with:
path: packages/rsc/bench-results.json
key: flight-bench-baseline-${{ github.sha }}

# ── Pull request: compare against main baseline ──────────────────────
- name: Restore baseline from main
if: github.event_name == 'pull_request'
id: flight-baseline
uses: actions/cache/restore@v4
with:
path: packages/rsc/flight-baseline.json
key: flight-bench-baseline-${{ github.event.pull_request.base.sha }}
restore-keys: flight-bench-baseline-

- name: Generate comparison report
if: github.event_name == 'pull_request' && steps.flight-baseline.outputs.cache-matched-key != ''
working-directory: packages/rsc
run: node __bench__/report.mjs --current bench-results.json --baseline flight-baseline.json --markdown comment.md --commit ${{ github.sha }}

- name: Post or update PR comment
if: github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
run: |
PR_NUMBER=${{ github.event.pull_request.number }}
REPO=${{ github.repository }}

# Find existing flight benchmark comment by HTML marker
COMMENT_ID=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
--paginate --jq '.[] | select(.body | contains("<!-- flight-bench-results -->")) | .id' \
| head -1)

if [ -n "$COMMENT_ID" ]; then
echo "Updating existing comment ${COMMENT_ID}"
gh api "repos/${REPO}/issues/comments/${COMMENT_ID}" \
-X PATCH \
-F "body=@packages/rsc/comment.md"
else
echo "Creating new comment"
gh pr comment "$PR_NUMBER" --body-file packages/rsc/comment.md
fi
2 changes: 1 addition & 1 deletion .github/workflows/upgrade-react-experimental.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ jobs:
**New version:** `${{ steps.latest.outputs.version }}`

### Updated locations
- `packages/react-server/package.json` — `react`, `react-dom`, `react-is`, `react-server-dom-webpack`
- `packages/react-server/package.json` — `react`, `react-dom`, `react-is`
- `package.json` — all `pnpm.overrides` for React
- `docs/` — micro-frontends MDX (en + ja) esm.sh URLs
- `pnpm-lock.yaml`
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ This separation has three structural costs:
2. **Artificial indirection.** The logical unit — "this server page contains this interactive widget" — is scattered across the filesystem. Code navigation and comprehension suffer.
3. **Impossible compositions.** A server function that dynamically constructs and returns different client components (e.g., a factory pattern) cannot exist — the server function and the client component must live in separate modules, so the server function cannot lexically define the client component it returns.

The third point is the most significant. RSC's serialization protocol (`react-server-dom-webpack`) already supports returning client component references from server functions. The limitation is entirely in the bundler — no existing tool can extract a `"use client"` component defined inside a `"use server"` function body.
The third point is the most significant. RSC's serialization protocol (implemented in `@lazarv/rsc`) already supports returning client component references from server functions. The limitation is entirely in the bundler — no existing tool can extract a `"use client"` component defined inside a `"use server"` function body.

<Link name="design-goals">
## Design Goals
Expand Down
12 changes: 4 additions & 8 deletions docs/src/pages/en/(pages)/features/micro-frontends.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,11 @@ To externalize the shared dependencies, you can use the `resolve.shared` option

When you specify the shared dependencies in the `react-server.config.json` file, the hosting application and the micro-frontends will load the shared dependencies from the specified remote URL.

For `@lazarv/react-server` to work properly with import maps, you need to specify the source for `react`, `react-jsx/runtime` (or `react/jsx-dev-runtime`), `react-dom`, `react-dom/client`, and `react-server-dom-webpack/client.browser` in the import map.
For `@lazarv/react-server` to work properly with import maps, you need to specify the source for `react`, `react-jsx/runtime` (or `react/jsx-dev-runtime`), `react-dom`, and `react-dom/client` in the import map.

If you want to use these dependencies from the CDN even during development, you can specify the source for these dependencies in the import map, but keep in mind that you need the development version of these dependencies.

> **Warning:** You need to use the exact same versions of `react`, `react/jsx-dev-runtime`, `react/jsx-runtime`, `react-dom`, `react-dom/client`, and `react-server-dom-webpack/client.browser` in the hosting application and the micro-frontends, both on the client and the server side. Otherwise, you may encounter compatibility issues.
> **Warning:** You need to use the exact same versions of `react`, `react/jsx-dev-runtime`, `react/jsx-runtime`, `react-dom`, and `react-dom/client` in the hosting application and the micro-frontends, both on the client and the server side. Otherwise, you may encounter compatibility issues.

To use an import map to work with both development and production environments, you need to create a configuration file for each environments, specifying the import maps. The configuration files below show how to use an import map that uses React from [esm.sh](https://esm.sh).

Expand All @@ -205,9 +205,7 @@ export default {
"react-dom":
"https://esm.sh/react-dom@0.0.0-experimental-705268dc-20260409?dev",
"react-dom/client":
"https://esm.sh/react-dom@0.0.0-experimental-705268dc-20260409/client?dev",
"react-server-dom-webpack/client.browser":
"https://esm.sh/react-server-dom-webpack@0.0.0-experimental-705268dc-20260409/client.browser?dev",
"https://esm.sh/react-dom@0.0.0-experimental-705268dc-20260409/client?dev"
},
},
};
Expand All @@ -225,9 +223,7 @@ export default {
"react-dom":
"https://esm.sh/react-dom@0.0.0-experimental-705268dc-20260409",
"react-dom/client":
"https://esm.sh/react-dom@0.0.0-experimental-705268dc-20260409/client",
"react-server-dom-webpack/client.browser":
"https://esm.sh/react-server-dom-webpack@0.0.0-experimental-705268dc-20260409/client.browser",
"https://esm.sh/react-dom@0.0.0-experimental-705268dc-20260409/client"
},
},
};
Expand Down
44 changes: 42 additions & 2 deletions docs/src/pages/en/(pages)/guide/design-decisions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ These decisions define the lowest layer of the runtime: which React version runs
</Link>

**Problem:**
React Server Components depend on an unstable wire format between `react-server-dom-webpack` and the React reconciler. The serialization protocol, flight format, and directive semantics ([`"use client"`](/guide/client-components), [`"use server"`](/guide/server-functions)) are not yet covered by React's public semver contract. A mismatch between the React version the runtime uses and the version the application installs causes silent serialization failures or runtime crashes.
React Server Components depend on an unstable wire format between the RSC serialization layer and the React reconciler. The serialization protocol, flight format, and directive semantics ([`"use client"`](/guide/client-components), [`"use server"`](/guide/server-functions)) are not yet covered by React's public semver contract. A mismatch between the React version the runtime uses and the version the application installs causes silent serialization failures or runtime crashes.

**Constraint:**
The RSC wire format must be version-locked across the server renderer, the client hydrator, and the flight encoder/decoder. Allowing user-installed React creates a combinatorial compatibility surface that cannot be tested reliably since React publishes experimental builds frequently with breaking internal changes.

**Decision:**
Bundle a specific React experimental build as a direct dependency. At startup, hijack module resolution so all imports of `react`, `react-dom`, `react/jsx-runtime`, and `react-server-dom-webpack` resolve to the runtime's bundled copies. On Node.js this uses `module.register()` with a custom loader; on Bun it uses `module.mock()`/`module.alias()`; on Deno it writes an import map to disk and respawns the process with `--import-map`.
Bundle a specific React experimental build as a direct dependency and use `@lazarv/rsc` — a bundler-agnostic RSC serialization layer — instead of `react-server-dom-webpack`. At startup, hijack module resolution so all imports of `react`, `react-dom`, and `react/jsx-runtime` resolve to the runtime's bundled copies. On Node.js this uses `module.register()` with a custom loader; on Bun it uses `module.mock()`/`module.alias()`; on Deno it writes an import map to disk and respawns the process with `--import-map`.

**Tradeoffs:**
- Users cannot upgrade React independently. Adoption of new React features is gated by a runtime release.
Expand All @@ -56,6 +56,46 @@ Bundle a specific React experimental build as a direct dependency. At startup, h
**Consequences:**
The wire format is guaranteed consistent across all render paths. Users do not need to install React — reducing dependency conflicts and version drift. But the runtime's release cadence becomes the bottleneck for React upgrades, and the three-way aliasing layer is a maintenance surface that must be verified against each runtime's release cycle.

<Link name="bundler-agnostic-rsc-serialization">
### Bundler-Agnostic RSC Serialization
</Link>

**Problem:**
React's official RSC serialization package, `react-server-dom-webpack`, is tightly coupled to Webpack. It depends on Webpack-specific module manifests for resolving `"use client"` and `"use server"` references, requires a Webpack plugin to generate those manifests at build time, and uses Webpack's `__webpack_require__` at runtime to load client modules. For a runtime built on [Vite](/integrations/vite) — which uses Rollup/Rolldown for production builds — this coupling creates a permanent impedance mismatch: every call through the serialization layer must shim, intercept, or rewrite Webpack-specific globals and module resolution paths that do not exist in the actual build output.

**Constraint:**
The replacement must implement the full React Flight wire protocol — the binary framing, row tags, type serialization (Elements, Promises, Maps, Sets, typed arrays, Blobs, ReadableStreams, async iterables, client/server references, temporary references, bound actions), error digest propagation, and the synchronous thenable contract that React's `use()` hook depends on. It must produce byte-identical wire output for all supported types so that existing React client code can consume the stream without modification. And it must run in any environment that supports Web Platform APIs — not just Node.js.

**Decision:**
Replace `react-server-dom-webpack` with [`@lazarv/rsc`](https://github.com/lazarv/react-server/tree/main/packages/rsc), a standalone Flight protocol implementation built exclusively on Web Platform APIs (`ReadableStream`, `WritableStream`, `TextEncoder`, `FormData`, `Blob`, `URL`). Instead of Webpack manifests, `@lazarv/rsc` exposes abstract `moduleResolver` and `moduleLoader` interfaces — the consumer provides resolution and loading logic, decoupling the serialization layer from any specific bundler. The package uses `Symbol.for()` to access React internal type tags rather than importing from `react` directly, making it compatible with any React version that uses the same symbol conventions.

```js
// Server: the consumer wires up their own module resolution
renderToReadableStream(element, {
moduleResolver: {
resolveClientReference(ref) {
return { id: ref.$$id, name: ref.$$name, chunks: [] };
},
},
});

// Client: the consumer wires up their own module loading
createFromReadableStream(stream, {
moduleLoader: {
requireModule(metadata) {
return moduleCache.get(metadata.id)?.[metadata.name];
},
},
});
```

**Tradeoffs:**
- The implementation must track upstream Flight protocol changes independently. When React adds new row tags, type support, or changes framing semantics in experimental builds, `@lazarv/rsc` must be updated to match — there is no automatic inheritance from React's codebase.
- The abstract `moduleResolver`/`moduleLoader` interfaces shift integration complexity from the serialization library to the consumer. For `@lazarv/react-server` this is handled by the Vite plugin layer, but standalone consumers must implement their own resolution logic.

**Consequences:**
The Webpack shim layer — global `__webpack_require__` interception, manifest generation, chunk ID rewriting — is eliminated entirely. Serialization performance improves substantially: benchmarks show 2–6× higher throughput on serialization (e.g., 659K vs 102K ops/s for minimal elements, 4.6K vs 777 ops/s for wide trees) and 1.2–11× on deserialization (e.g., 44K vs 3.9K ops/s for shallow-wide trees), with roundtrip gains of 2–6× across representative payloads. The same serialization code runs identically on Node.js, Bun, Deno, Cloudflare Workers, and in the browser — no platform-specific entry points or conditional imports. Because the interfaces are abstract, the runtime is free to evolve its build tooling (e.g., migrating from Rollup to Rolldown) without touching the serialization layer. And critically, the same code path executes in both development and production — unlike `react-server-dom-webpack`, which ships separate development and production bundles with different code paths, `@lazarv/rsc` has a single implementation with no environment-conditional branches, eliminating an entire class of dev/prod divergence bugs.

<Link name="multi-runtime-execution">
### Multi-Runtime Execution
</Link>
Expand Down
6 changes: 3 additions & 3 deletions docs/src/pages/en/(pages)/integrations/react.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import Link from "../../../../components/Link.jsx";

React Server Components require an experimental build of React that includes the RSC wire protocol, server references, and streaming support. These experimental APIs are not available in the stable releases published to npm. By bundling the exact React version it was built against, `@lazarv/react-server` guarantees:

- **Correct RSC support** — the `react-server` export condition, `react-server-dom-webpack`, and streaming primitives all match.
- **Zero configuration** — you don't need to figure out which experimental React build to install, or keep it in sync across `react`, `react-dom`, and `react-server-dom-webpack`.
- **Correct RSC support** — the `react-server` export condition, `@lazarv/rsc` serialization layer, and streaming primitives all match.
- **Zero configuration** — you don't need to figure out which experimental React build to install, or keep it in sync across `react`, `react-dom`, and the RSC serialization layer.
- **Single React instance** — the runtime ensures only one copy of React is loaded at any time, avoiding the notorious "multiple React instances" bugs (broken hooks, broken context, etc.).

<Link name="no-react-in-your-package-json">
Expand Down Expand Up @@ -46,7 +46,7 @@ pnpm remove react react-dom
## How it works
</Link>

`@lazarv/react-server` lists `react`, `react-dom`, and `react-server-dom-webpack` as direct **dependencies** (not peer dependencies). At both development and build time, the runtime sets up module aliases so that any `import` of `react` or `react-dom` — whether from your code or from third-party libraries — resolves to the React version bundled with the runtime.
`@lazarv/react-server` lists `react` and `react-dom` as direct **dependencies** (not peer dependencies) and uses `@lazarv/rsc` — a bundler-agnostic RSC serialization layer — instead of `react-server-dom-webpack`. At both development and build time, the runtime sets up module aliases so that any `import` of `react` or `react-dom` — whether from your code or from third-party libraries — resolves to the React version bundled with the runtime.

This aliasing operates at the Vite module resolution level across all three environments:

Expand Down
Loading
Loading