Skip to content

feat: hydration islands#429

Merged
lazarv merged 2 commits into
mainfrom
feat/hydration-islands
May 20, 2026
Merged

feat: hydration islands#429
lazarv merged 2 commits into
mainfrom
feat/hydration-islands

Conversation

@lazarv
Copy link
Copy Markdown
Owner

@lazarv lazarv commented May 20, 2026

Summary

This PR adds first-class hydration islands to @lazarv/react-server through 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 with hydrateRoot. On pages that already have a PAGE_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 a ReactServerComponent for the island outlet without creating another root.

The island runtime supports load, idle, visible, interaction, media, and never strategies. 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 Link navigation and Refresh without 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-islands app demonstrates rootless hydration islands, mixed PAGE_ROOT mode, every supported strategy, visible client-component loading, RSC refresh, and local outlet navigation through Link.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
react-server-docs 5364859 May 20 2026, 12:05 PM

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 20, 2026

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
1123 2 1121 6
View the top 3 failed test(s) by shortest run time
__test__/deno.spec.mjs > create-react-server: deno runtime (pnpm) > preset: blank > starts in production mode
Stack Traces | 0.00109s run time
AssertionError: production start should work: expected false to be true // Object.is equality

- Expected
+ Received

- true
+ false

 ❯ __test__/deno.spec.mjs:74:66
__test__/deno.spec.mjs > create-react-server: deno runtime (npm) > preset: nextjs > starts in production mode
Stack Traces | 0.00112s run time
AssertionError: production start should work: expected false to be true // Object.is equality

- Expected
+ Received

- true
+ false

 ❯ __test__/deno.spec.mjs:74:66
__test__/deno.spec.mjs > create-react-server: deno runtime (pnpm) > preset: blank > builds the app
Stack Traces | 0.00224s run time
AssertionError: build should succeed: expected false to be true // Object.is equality

- Expected
+ Received

- true
+ false

 ❯ __test__/deno.spec.mjs:70:58
__test__/deno.spec.mjs > create-react-server: deno runtime (npm) > preset: nextjs > builds the app
Stack Traces | 0.00706s run time
AssertionError: build should succeed: expected false to be true // Object.is equality

- Expected
+ Received

- true
+ false

 ❯ __test__/deno.spec.mjs:70:58
__test__/deno.spec.mjs > create-react-server: deno runtime (pnpm) > preset: blank > dev mode starts and serves the app
Stack Traces | 0.00731s run time
AssertionError: dev mode should work: expected false to be true // Object.is equality

- Expected
+ Received

- true
+ false

 ❯ __test__/deno.spec.mjs:66:56

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@github-actions
Copy link
Copy Markdown

⚡ Flight Protocol Benchmark

Commit: a7cad32

Serialization (renderToReadableStream)

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.

@github-actions
Copy link
Copy Markdown

⚡ Benchmark Results

PR a721180 main 9cd6bb6
Config 50 connections, 10s/test 50 connections, 10s/test
Benchmark Req/s vs main Avg Latency vs main P99 Latency Throughput
minimal 1415 🔴 -5.7% 34.67 ms 🔴 +6.0% 73 ms 0.9 MB/s
small 1400 🔴 -5.7% 35.14 ms 🔴 +6.2% 63 ms 1.4 MB/s
medium 400 🔴 -2.5% 123.93 ms 🔴 +2.5% 179 ms 5.9 MB/s
large 49 ⚪ -0.9% 981.87 ms 🔴 +1.2% 1954 ms 5.0 MB/s
deep 937 🔴 -3.1% 52.67 ms 🔴 +3.3% 87 ms 3.2 MB/s
wide 71 🟢 +1.6% 663.8 ms 🟢 -5.8% 1118 ms 3.9 MB/s
cached 3547 🟢 +4.6% 13.6 ms 🟢 -4.4% 26 ms 52.1 MB/s
client-min 532 🔴 -4.2% 92.94 ms 🔴 +4.7% 146 ms 2.2 MB/s
client-small 548 🔴 -2.8% 90.07 ms 🔴 +2.8% 130 ms 2.5 MB/s
client-med 386 🔴 -5.8% 127.67 ms 🔴 +5.9% 196 ms 7.1 MB/s
client-large 83 ⚪ -0.9% 564.35 ms ⚪ -0.5% 958 ms 8.8 MB/s
client-deep 493 🔴 -6.2% 100.24 ms 🔴 +6.6% 148 ms 3.5 MB/s
client-wide 146 🟢 +3.5% 339.05 ms 🟢 -2.2% 574 ms 8.5 MB/s
rsc-client-large 1231 🔴 -2.3% 39.97 ms 🔴 +2.3% 62 ms 3.1 MB/s
rsc-client-wide 1272 🔴 -1.8% 38.72 ms 🔴 +2.0% 57 ms 3.2 MB/s
static-json 10090 🔴 -9.6% 4.64 ms 🔴 +23.1% 14 ms 4.2 MB/s
static-js 9710 🔴 -10.3% 4.77 ms 🔴 +20.2% 14 ms 12.2 MB/s
404-miss 5345 🔴 -16.8% 8.96 ms 🔴 +25.5% 18 ms 0.7 MB/s
hybrid-min 529 🔴 -6.2% 93.7 ms 🔴 +7.1% 141 ms 2.5 MB/s
hybrid-small 525 🔴 -1.6% 94.25 ms 🔴 +1.7% 141 ms 3.0 MB/s
hybrid-medium 256 🔴 -3.2% 192.95 ms 🔴 +3.4% 263 ms 10.9 MB/s
hybrid-large 41 🔴 -6.7% 1118.37 ms 🔴 +7.4% 1981 ms 13.4 MB/s
hybrid-deep 407 🔴 -2.1% 121.8 ms 🔴 +2.0% 185 ms 5.6 MB/s
hybrid-wide 63 🔴 -3.1% 732.51 ms 🟢 -2.5% 1361 ms 12.5 MB/s
hybrid-cached 3011 🔴 -7.5% 16.13 ms 🔴 +9.1% 31 ms 127.9 MB/s
hybrid-client-min 550 🔴 -6.9% 89.83 ms 🔴 +7.5% 141 ms 2.4 MB/s
hybrid-client-small 558 🔴 -6.0% 88.93 ms 🔴 +6.8% 132 ms 2.6 MB/s
hybrid-client-medium 393 🔴 -4.4% 125.61 ms 🔴 +4.8% 183 ms 7.3 MB/s
hybrid-client-large 85 ⚪ +0.2% 572.15 ms ⚪ -0.5% 1137 ms 8.9 MB/s
hybrid-client-deep 495 🔴 -7.3% 100.08 ms 🔴 +8.0% 145 ms 3.5 MB/s
hybrid-client-wide 142 ⚪ +0.1% 342.98 ms 🟢 -1.2% 612 ms 8.3 MB/s
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.

@lazarv lazarv merged commit c8e529f into main May 20, 2026
65 of 67 checks passed
@lazarv lazarv deleted the feat/hydration-islands branch May 20, 2026 12:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants