feat: hydration islands#429
Conversation
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
react-server-docs | 5364859 | May 20 2026, 12:05 PM |
❌ 2 Tests Failed:
View the top 3 failed test(s) by shortest run time
To view more test analytics, go to the Test Analytics Dashboard |
⚡ Flight Protocol BenchmarkCommit: Serialization (
|
| Scenario | @lazarv/rsc | webpack | vs webpack |
|---|---|---|---|
| react: minimal element | 211.4K | 27.9K | 🟢 +657.7% |
| react: shallow wide (1000) | 2.2K | 349 | 🟢 +522.9% |
| react: deep nested (100) | 17.2K | 6.0K | 🟢 +187.5% |
| react: product list (50) | 5.9K | 2.0K | 🟢 +194.4% |
| react: large table (500x10) | 267 | 92 | 🟢 +191.5% |
| data: primitives | 172.3K | 38.7K | 🟢 +345.8% |
| data: large string (100KB) | 7.2K | 6.4K | 🟢 +12.3% |
| data: nested objects (20) | 56.9K | 25.0K | 🟢 +127.8% |
| data: large array (10K) | 114 | 107 | 🟢 +5.7% |
| data: Map & Set | 10.6K | 5.6K | 🟢 +89.3% |
| data: Date/BigInt/Symbol | 156.8K | 31.9K | 🟢 +391.4% |
| data: typed arrays | 30.6K | 12.8K | 🟢 +139.1% |
| data: mixed payload | 8.3K | 3.9K | 🟢 +113.2% |
Prerender (prerender)
| Scenario | @lazarv/rsc ops/s | mean |
|---|---|---|
| react: minimal element | 247.1K | 4.0 µs |
| react: shallow wide (1000) | 2.0K | 512.2 µs |
| react: deep nested (100) | 16.0K | 62.4 µs |
| react: product list (50) | 5.7K | 176.0 µs |
| react: large table (500x10) | 268 | 3.73 ms |
| data: primitives | 187.0K | 5.3 µs |
| data: large string (100KB) | 679 | 1.47 ms |
| data: nested objects (20) | 56.7K | 17.6 µs |
| data: large array (10K) | 116 | 8.65 ms |
| data: Map & Set | 11.2K | 89.5 µs |
| data: Date/BigInt/Symbol | 177.6K | 5.6 µs |
| data: typed arrays | 649 | 1.54 ms |
| data: mixed payload | 7.5K | 132.7 µs |
Deserialization (createFromReadableStream)
| Scenario | @lazarv/rsc | webpack | vs webpack |
|---|---|---|---|
| react: minimal element | 164.0K | 135.0K | 🟢 +21.4% |
| react: shallow wide (1000) | 20.7K | 2.0K | 🟢 +952.2% |
| react: deep nested (100) | 102.6K | 19.2K | 🟢 +433.2% |
| react: product list (50) | 49.6K | 14.7K | 🟢 +237.8% |
| react: large table (500x10) | 4.0K | 2.0K | 🟢 +100.8% |
| data: primitives | 137.2K | 128.0K | 🟢 +7.2% |
| data: large string (100KB) | 38.1K | 32.8K | 🟢 +16.3% |
| data: nested objects (20) | 82.8K | 70.1K | 🟢 +18.2% |
| data: large array (10K) | 285 | 237 | 🟢 +19.9% |
| data: Map & Set | 16.1K | 14.2K | 🟢 +13.9% |
| data: Date/BigInt/Symbol | 134.8K | 110.5K | 🟢 +22.0% |
| data: typed arrays | 52.3K | 44.2K | 🟢 +18.5% |
| data: mixed payload | 25.3K | 14.9K | 🟢 +69.9% |
Roundtrip (serialize + deserialize)
| Scenario | @lazarv/rsc | webpack | vs webpack |
|---|---|---|---|
| react: minimal element | 104.3K | 19.9K | 🟢 +424.1% |
| react: shallow wide (1000) | 1.7K | 289 | 🟢 +486.2% |
| react: deep nested (100) | 14.4K | 4.3K | 🟢 +234.7% |
| react: product list (50) | 5.2K | 1.7K | 🟢 +214.8% |
| react: large table (500x10) | 256 | 82 | 🟢 +212.1% |
| data: primitives | 82.7K | 27.8K | 🟢 +197.0% |
| data: large string (100KB) | 6.2K | 6.2K | ⚪ +0.1% |
| data: nested objects (20) | 33.4K | 16.3K | 🟢 +104.4% |
| data: large array (10K) | 79 | 68 | 🟢 +16.1% |
| data: Map & Set | 6.0K | 3.8K | 🟢 +59.8% |
| data: Date/BigInt/Symbol | 67.9K | 20.3K | 🟢 +233.9% |
| data: typed arrays | 23.8K | 10.6K | 🟢 +125.4% |
| data: mixed payload | 5.9K | 2.9K | 🟢 +101.9% |
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.
⚡ 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 PR adds first-class hydration islands to
@lazarv/react-serverthrough a new inline"use hydrate"directive. A component marked with this directive is rendered as normal server HTML during the initial response, but its interactive React tree is isolated into island-scoped hydration data and hydrated later as a local non-root outlet. This allows mostly static pages to keep the page root unhydrated while selected subtrees become interactive according to an explicit strategy.Implementation
The directive transform extracts
"use hydrate"components in the same style as the existing inline directive pipeline and rewrites them to an internal hydration island component. On the server, the island renderer produces the initial HTML and a separate RSC payload for the island. In rootless pages, the client entry detects island markers and hydrates each island withhydrateRoot. On pages that already have aPAGE_ROOT, the island is represented by a client boundary inside the existing React tree; that boundary reads request-scoped hydration data, waits for the selected strategy when needed, and then renders aReactServerComponentfor the island outlet without creating another root.The island runtime supports
load,idle,visible,interaction,media, andneverstrategies. Deferred strategies can delay both hydration and client module loading, and the manifest collection path now skips initial modulepreload entries for client modules that are only imported by deferred hydration island modules. When a"use hydrate"component appears later inside an RSC navigation or update payload, it intentionally renders as plain component output instead of creating a new island, because that subtree is already owned by the existing React tree.Hydration islands are registered as local outlets, so hydrated islands can use local
Linknavigation andRefreshwithout navigating or hydrating the page root. DevTools now identifies these outlets as islands rather than remotes and exposes their hydration state so pending, scheduled, hydrating, and hydrated islands are visible in the outlets panel.Documentation and Example
The docs add a new Hydration Islands feature page that explains how islands differ from client components and PPR, documents each hydration strategy, and describes local outlet navigation, browser history behavior, and DevTools support. The comparison page was updated to reflect hydration islands, HTTP/runtime capabilities, observability, server function defenses, and server function middleware tradeoffs across React Server, Next.js, TanStack Start, React Router, Waku, and Astro where relevant. The LLM reference and React Server skill were also updated so agents know about the new directive and rendering model.
A new
examples/hydration-islandsapp demonstrates rootless hydration islands, mixedPAGE_ROOTmode, every supported strategy, visible client-component loading, RSC refresh, and local outlet navigation throughLink.