hydration-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseHydration Patterns
Hydration模式
What is Hydration?
什么是Hydration(水合)?
Hydration is the process of attaching JavaScript event handlers to server-rendered HTML, making static content interactive.
Server-Rendered HTML (static) → JavaScript Attaches → Interactive Application
"Dry HTML" → Hydration → "Hydrated App"Hydration是将JavaScript事件处理程序附加到服务端渲染的HTML上,使静态内容具备交互性的过程。
Server-Rendered HTML (static) → JavaScript Attaches → Interactive Application
"Dry HTML" → Hydration → "Hydrated App"The Hydration Process
Hydration的流程
Step by Step
分步解析
1. Server renders HTML with content
2. Browser displays HTML immediately (fast FCP)
3. Browser downloads JavaScript bundle
4. JavaScript parses and executes
5. Framework "hydrates":
- Walks the DOM
- Attaches event listeners
- Initializes state
- Makes components interactive
6. App is now fully interactive (TTI reached)1. 服务端渲染带内容的HTML
2. 浏览器立即显示HTML(快速实现FCP)
3. 浏览器下载JavaScript包
4. JavaScript解析并执行
5. 框架执行“水合”操作:
- 遍历DOM
- 附加事件监听器
- 初始化状态
- 使组件具备交互性
6. 应用现在完全可交互(达到TTI)The Hydration Gap
Hydration间隙
The time between content visible (FCP) and interactive (TTI):
Timeline:
[------- Server Render -------][-- HTML Sent --][---- JS Download ----][-- Hydration --]
↓ ↓
Content Visible Interactive
[========= Hydration Gap =========]
(Page looks ready but isn't)内容可见(FCP)到可交互(TTI)之间的时间:
时间线:
[------- 服务端渲染 -------][-- HTML传输 --][---- JS下载 ----][-- Hydration --]
↓ ↓
内容可见 可交互
[========= Hydration间隙 =========]
(页面看起来就绪但实际不可交互)Hydration Challenges
Hydration的挑战
1. Time to Interactive (TTI) Delay
1. 可交互时间(TTI)延迟
Even though content is visible, buttons don't work until hydration completes.
User sees "Buy Now" button → Clicks → Nothing happens → Frustrated
(hydration not complete)即使内容可见,按钮也要等到Hydration完成后才能工作。
用户看到“立即购买”按钮 → 点击 → 无反应 → 感到受挫
(Hydration尚未完成)2. Hydration Mismatch
2. Hydration不匹配
If client render differs from server render:
javascript
// Server: <div>Today is Monday</div>
// Client (different timezone): <div>Today is Tuesday</div>
// Result: Error or UI flicker如果客户端渲染结果与服务端渲染结果不同:
javascript
// 服务端: <div>今天是周一</div>
// 客户端(不同时区): <div>今天是周二</div>
// 结果: 报错或UI闪烁3. Bundle Size
3. 包体积过大
Full hydration requires downloading ALL component code:
Page uses: Header, Hero, ProductList, Footer, Cart, Reviews...
Bundle includes: ALL components (even if user never interacts)全水合需要下载所有组件代码:
页面使用: 头部、Hero区、产品列表、页脚、购物车、评论...
包包含: 所有组件(即使用户从不交互)4. CPU Cost
4. CPU开销
Hydration walks the entire DOM and attaches listeners:
Large page = More DOM nodes = Longer hydration = Worse TTIHydration会遍历整个DOM并附加监听器:
页面越大 = DOM节点越多 = Hydration耗时越长 = TTI表现越差Full Hydration
全水合
Traditional approach: hydrate the entire page.
javascript
// React example
import { hydrateRoot } from 'react-dom/client';
import App from './App';
// Hydrates entire app
hydrateRoot(document.getElementById('root'), <App />);传统方案:对整个页面执行水合。
javascript
// React示例
import { hydrateRoot } from 'react-dom/client';
import App from './App';
// 对整个应用执行水合
hydrateRoot(document.getElementById('root'), <App />);Pros and Cons
优缺点对比
| Pros | Cons |
|---|---|
| Simple mental model | Large JS bundles |
| Full interactivity everywhere | Slow TTI on large pages |
| Framework handles everything | Wasted JS for static content |
| 优点 | 缺点 |
|---|---|
| 思维模型简单 | JS包体积大 |
| 所有区域都具备全交互性 | 大型页面TTI慢 |
| 框架处理所有细节 | 静态内容的JS被浪费 |
Progressive Hydration
渐进式水合
Hydrate components in priority order, deferring non-critical parts.
按优先级对组件执行水合,延迟处理非关键部分。
Strategy
策略
1. Hydrate above-the-fold content first
2. Hydrate below-the-fold on scroll or idle
3. Hydrate low-priority on user interaction1. 首先对首屏内容执行水合
2. 滚动到或浏览器空闲时对首屏以下内容执行水合
3. 用户交互时对低优先级内容执行水合Implementation Pattern
实现模式
javascript
// Conceptual - defer hydration until visible
function LazyHydrate({ children, whenVisible }) {
const [hydrated, setHydrated] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setHydrated(true);
}
});
observer.observe(ref.current);
return () => observer.disconnect();
}, []);
if (!hydrated) return <div ref={ref}>{/* static HTML */}</div>;
return children;
}javascript
// 概念示例 - 延迟水合直到内容可见
function LazyHydrate({ children, whenVisible }) {
const [hydrated, setHydrated] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setHydrated(true);
}
});
observer.observe(ref.current);
return () => observer.disconnect();
}, []);
if (!hydrated) return <div ref={ref}>{/* 静态HTML */}</div>;
return children;
}Timeline
时间线
[Initial Hydration - Critical Components Only]
↓
[Scroll/Idle - Hydrate More Components]
↓
[Interaction - Hydrate On Demand][初始水合 - 仅关键组件]
↓
[滚动/空闲时 - 水合更多组件]
↓
[用户交互时 - 按需水合]Selective Hydration
选择性水合
Only hydrate components that need interactivity.
React 18+ with Suspense enables this:
jsx
<Suspense fallback={<Loading />}>
<Comments /> {/* Hydrates independently when ready */}
</Suspense>仅对需要交互的组件执行水合。
React 18+搭配Suspense可实现此方案:
jsx
<Suspense fallback={<Loading />}>
<Comments /> {/* 准备就绪后独立执行水合 */}
</Suspense>How It Works
工作原理
Server streams: Header (hydrated) → Content (hydrated) → Comments (pending)
Comments data arrives → Comments hydrate independently
User clicks comment before hydration → React prioritizes that subtree服务端流式传输: 头部(已水合)→ 内容(已水合)→ 评论(待处理)
评论数据到达 → 评论独立执行水合
用户在水合完成前点击评论 → React优先处理该子树Partial Hydration
部分水合
Ship zero JavaScript for static components.
静态组件不打包任何JavaScript。
The Insight
核心思路
Most pages have static and interactive parts:
┌─────────────────────────────────────┐
│ Header (static) │ ← No JS needed
├─────────────────────────────────────┤
│ Hero Banner (static) │ ← No JS needed
├─────────────────────────────────────┤
│ Product List (static) │ ← No JS needed
├─────────────────────────────────────┤
│ Add to Cart Button ★ │ ← NEEDS JS
├─────────────────────────────────────┤
│ Reviews (static) │ ← No JS needed
├─────────────────────────────────────┤
│ Newsletter Signup ★ │ ← NEEDS JS
├─────────────────────────────────────┤
│ Footer (static) │ ← No JS needed
└─────────────────────────────────────┘With partial hydration: Only "Add to Cart" and "Newsletter" components ship JS.
大多数页面都包含静态和交互部分:
┌─────────────────────────────────────┐
│ 头部(静态) │ ← 无需JS
├─────────────────────────────────────┤
│ Hero横幅(静态) │ ← 无需JS
├─────────────────────────────────────┤
│ 产品列表(静态) │ ← 无需JS
├─────────────────────────────────────┤
│ 加入购物车按钮 ★ │ ← 需要JS
├─────────────────────────────────────┤
│ 评论(静态) │ ← 无需JS
├─────────────────────────────────────┤
│ 订阅新闻通讯 ★ │ ← 需要JS
├─────────────────────────────────────┤
│ 页脚(静态) │ ← 无需JS
└─────────────────────────────────────┘使用部分水合后: 仅“加入购物车”和“订阅新闻通讯”组件会打包JS。
Islands Architecture
岛屿架构
Independent interactive components in a sea of static HTML.
在静态HTML的“海洋”中嵌入独立的交互式组件。
Concept
概念
┌─────────────────────────────────────────────────┐
│ │
│ Static HTML (server-rendered, no JS) │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Island │ │ Island │ │
│ │ (Counter) │ │ (Search) │ │
│ │ [JS: 2KB] │ │ [JS: 5KB] │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ Static content continues... │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Island (Comments) │ │
│ │ [JS: 8KB] │ │
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────┘
Total JS: 15KB (vs potentially 200KB+ for full hydration)┌─────────────────────────────────────────────────┐
│ │
│ 静态HTML(服务端渲染,无JS) │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 岛屿 │ │ 岛屿 │ │
│ │ (计数器) │ │ (搜索框) │ │
│ │ [JS: 2KB] │ │ [JS: 5KB] │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ 静态内容继续... │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 岛屿(评论区) │ │
│ │ [JS: 8KB] │ │
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────┘
总JS体积: 15KB(相比全水合可能的200KB+)Key Characteristics
核心特性对比
| Aspect | Full Hydration | Islands |
|---|---|---|
| JS shipped | All components | Only interactive |
| Hydration | Entire page | Per island |
| Bundle size | Large | Minimal |
| TTI | Slow | Fast |
| Coupling | Components connected | Islands independent |
| 维度 | 全水合 | 岛屿架构 |
|---|---|---|
| 打包的JS | 所有组件 | 仅交互式组件 |
| Hydration范围 | 整个页面 | 每个岛屿独立 |
| 包体积 | 大 | 极小 |
| TTI | 慢 | 快 |
| 耦合度 | 组件相互关联 | 岛屿彼此独立 |
Frameworks Using Islands
支持岛屿架构的框架
- Astro: First-class islands support
- Fresh (Deno): Islands by default
- Îles: Islands for Vite
- Qwik: Resumability (related concept)
- Astro: 原生支持岛屿架构
- Fresh (Deno): 默认采用岛屿模式
- Îles: 面向Vite的岛屿框架
- Qwik: 具备可恢复性(相关概念)
Island Definition Example (Astro)
岛屿定义示例(Astro)
astro
---
// Static by default
import Header from './Header.astro';
import Counter from './Counter.jsx'; // React component
---
<Header /> <!-- No JS shipped -->
<Counter client:load /> <!-- JS shipped, hydrates on load -->
<Counter client:visible /> <!-- JS shipped, hydrates when visible -->
<Counter client:idle /> <!-- JS shipped, hydrates on browser idle -->astro
---
// 默认静态
import Header from './Header.astro';
import Counter from './Counter.jsx'; // React组件
---
<Header /> <!-- 不打包JS -->
<Counter client:load /> <!-- 打包JS,加载时执行水合 -->
<Counter client:visible /> <!-- 打包JS,可见时执行水合 -->
<Counter client:idle /> <!-- 打包JS,浏览器空闲时执行水合 -->Resumability (Qwik's Approach)
可恢复性(Qwik的方案)
Skip hydration entirely by serializing application state.
通过序列化应用状态完全跳过Hydration步骤。
Traditional Hydration
传统Hydration
Server renders → Client downloads JS → Re-executes to rebuild state → Attaches listeners服务端渲染 → 客户端下载JS → 重新执行代码重建状态 → 附加监听器Resumability
可恢复性
Server renders + serializes state → Client downloads minimal JS → Resumes exactly where server left off服务端渲染 + 序列化状态 → 客户端下载极小体积JS → 直接从服务端的状态继续执行Key Difference
核心差异
javascript
// Hydration: Re-run component code to figure out handlers
// Resumability: Handlers serialized in HTML, just attach them
// Qwik serializes even closures:
<button on:click="./chunk.js#handleClick[0]">Click me</button>javascript
// Hydration: 重新运行组件代码以确定处理程序
// 可恢复性: 处理程序已序列化到HTML中,只需附加即可
// Qwik甚至可以序列化闭包:
<button on:click="./chunk.js#handleClick[0]">点击我</button>Choosing a Hydration Strategy
选择Hydration策略
| Scenario | Recommended Pattern |
|---|---|
| SPA/Dashboard | Full hydration |
| Content site with some interactivity | Islands |
| E-commerce product page | Partial/Progressive |
| Marketing landing page | Islands or SSG (no hydration) |
| Highly interactive app | Full hydration with code splitting |
| Performance critical, varied interactivity | Islands or Resumability |
| 场景 | 推荐方案 |
|---|---|
| SPA/仪表盘 | 全水合 |
| 带少量交互的内容型网站 | 岛屿架构 |
| 电商产品页 | 部分/渐进式水合 |
| 营销落地页 | 岛屿架构或SSG(无Hydration) |
| 高交互性应用 | 带代码分割的全水合 |
| 性能敏感、交互复杂度多变 | 岛屿架构或可恢复性方案 |
Hydration Anti-Patterns
Hydration反模式
1. Hydration Mismatch
1. Hydration不匹配
javascript
// Bad: Different output on server vs client
function Greeting() {
return <p>Hello at {new Date().toLocaleTimeString()}</p>;
}
// Good: Defer client-only values
function Greeting() {
const [time, setTime] = useState(null);
useEffect(() => setTime(new Date().toLocaleTimeString()), []);
return <p>Hello{time && ` at ${time}`}</p>;
}javascript
// 错误示例: 服务端与客户端输出不同
function Greeting() {
return <p>当前时间: {new Date().toLocaleTimeString()}</p>;
}
// 正确示例: 延迟客户端专属值的渲染
function Greeting() {
const [time, setTime] = useState(null);
useEffect(() => setTime(new Date().toLocaleTimeString()), []);
return <p>当前时间: {time && ` ${time}`}</p>;
}2. Blocking Hydration on Data
2. 因数据请求阻塞Hydration
javascript
// Bad: Hydration waits for fetch
useEffect(() => {
fetchData().then(setData); // Delays hydration
}, []);
// Good: Use server-provided data or streamingjavascript
// 错误示例: Hydration等待请求完成
useEffect(() => {
fetchData().then(setData); // 延迟Hydration
}, []);
// 正确示例: 使用服务端提供的数据或流式传输3. Huge Hydration Bundles
3. 过大的Hydration包
javascript
// Bad: Import everything at top level
import { FullCalendar } from 'massive-calendar-lib';
// Good: Dynamic import for heavy components
const Calendar = lazy(() => import('./Calendar'));javascript
// 错误示例: 顶部导入所有内容
import { FullCalendar } from 'massive-calendar-lib';
// 正确示例: 动态导入重型组件
const Calendar = lazy(() => import('./Calendar'));Deep Dive: Understanding Hydration From First Principles
深入解析:从底层原理理解Hydration
Why Hydration Exists: The Core Problem
Hydration存在的原因:核心矛盾
Hydration exists because of a fundamental mismatch between two goals:
GOAL 1: Fast First Paint (show content quickly)
→ Server rendering gives HTML immediately
→ Browser displays without JavaScript
GOAL 2: Rich Interactivity (React/Vue/Svelte apps)
→ Requires JavaScript
→ Event handlers, state, reactivity
THE CONFLICT:
Server-rendered HTML is STATIC
Your framework needs to MANAGE that HTML
THE SOLUTION:
"Hydration" - attach framework to existing HTMLHydration的存在是为了解决两个目标之间的根本矛盾:
目标1: 快速首次绘制(快速展示内容)
→ 服务端渲染可立即提供HTML
→ 浏览器无需JavaScript即可显示
目标2: 丰富的交互性(React/Vue/Svelte应用)
→ 需要JavaScript
→ 事件处理程序、状态、响应式能力
矛盾点:
服务端渲染的HTML是静态的
框架需要管理这些HTML
解决方案:
"Hydration" - 将框架与现有HTML关联What Hydration Actually Does Internally
Hydration内部实际执行的操作
Let's trace through React's hydration process step by step:
javascript
// 1. Server rendered this HTML:
`<button id="counter">Count: 0</button>`
// 2. Client receives HTML and displays it (fast!)
// 3. JavaScript bundle loads and executes
// 4. React's hydrateRoot runs:
hydrateRoot(document.getElementById('root'), <App />);
// 5. React executes your component to get expected virtual DOM:
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}
// Expected virtual DOM:
// { type: 'button', props: { onClick: fn }, children: ['Count: ', 0] }
// 6. React walks EXISTING DOM and COMPARES to virtual DOM:
const existingButton = document.querySelector('button');
// Does it match? Yes, same structure
// 7. React ATTACHES the onClick handler to existing button:
existingButton.addEventListener('click', () => setCount(c => c + 1));
// 8. React connects state management:
// - useState now tracks count
// - setCount will trigger re-renders
// - Component is now "live"The key insight: Hydration doesn't recreate the DOM. It walks the existing DOM and "wires up" the JavaScript.
我们一步步追踪React的Hydration流程:
javascript
// 1. 服务端渲染出如下HTML:
`<button id="counter">计数: 0</button>`
// 2. 客户端接收HTML并显示(速度很快!)
// 3. JavaScript包加载并执行
// 4. React的hydrateRoot运行:
hydrateRoot(document.getElementById('root'), <App />);
// 5. React执行组件以获取预期的虚拟DOM:
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>计数: {count}</button>;
}
// 预期的虚拟DOM:
// { type: 'button', props: { onClick: fn }, children: ['计数: ', 0] }
// 6. React遍历现有DOM并与虚拟DOM对比:
const existingButton = document.querySelector('button');
// 是否匹配?是的,结构一致
// 7. React将onClick处理程序附加到现有按钮:
existingButton.addEventListener('click', () => setCount(c => c + 1));
// 8. React连接状态管理:
// - useState现在追踪count值
// - setCount会触发重新渲染
// - 组件现在“激活”了核心要点: Hydration不会重建DOM。它会遍历现有DOM并“连接”JavaScript逻辑。
The Hydration Mismatch Problem in Detail
Hydration不匹配问题的详细解析
When server HTML doesn't match what client would render:
javascript
// Server renders at 11:59:59 PM December 31:
function NewYearCountdown() {
const now = new Date();
return <p>Current time: 11:59:59 PM</p>;
}
// Client hydrates at 12:00:01 AM January 1:
function NewYearCountdown() {
const now = new Date(); // Different time!
return <p>Current time: 12:00:01 AM</p>;
}
// React compares:
// Server HTML: "Current time: 11:59:59 PM"
// Client expected: "Current time: 12:00:01 AM"
// MISMATCH!What happens on mismatch:
DEVELOPMENT MODE:
- Console warning: "Text content did not match"
- React shows the client version (visual jump)
PRODUCTION MODE:
- React silently patches to client version
- Can cause visual flicker
- SEO mismatch if crawled during gap
SEVERE MISMATCH:
- Different DOM structure, not just text
- React may throw errors
- App may break completelyThe fix pattern:
javascript
function NewYearCountdown() {
const [time, setTime] = useState(null); // null on server
useEffect(() => {
// Only runs on client
setTime(new Date().toLocaleTimeString());
const interval = setInterval(() => {
setTime(new Date().toLocaleTimeString());
}, 1000);
return () => clearInterval(interval);
}, []);
return <p>Current time: {time ?? 'Loading...'}</p>;
}
// Server renders: "Current time: Loading..."
// Client hydrates to same: "Current time: Loading..."
// useEffect updates to real time (no mismatch)当服务端HTML与客户端渲染结果不同时:
javascript
// 服务端在12月31日23:59:59渲染:
function NewYearCountdown() {
const now = new Date();
return <p>当前时间: 23:59:59</p>;
}
// 客户端在1月1日00:00:01执行Hydration:
function NewYearCountdown() {
const now = new Date(); // 时间不同!
return <p>当前时间: 00:00:01</p>;
}
// React对比:
// 服务端HTML: "当前时间: 23:59:59"
// 客户端预期: "当前时间: 00:00:01"
// 不匹配!不匹配时的表现:
开发模式:
- 控制台警告: "文本内容不匹配"
- React显示客户端版本(视觉跳转)
生产模式:
- React静默修复为客户端版本
- 可能导致视觉闪烁
- 间隙期被爬取会导致SEO不匹配
严重不匹配:
- DOM结构不同,而非仅文本差异
- React可能抛出错误
- 应用可能完全崩溃修复方案:
javascript
function NewYearCountdown() {
const [time, setTime] = useState(null); // 服务端渲染为null
useEffect(() => {
// 仅在客户端运行
setTime(new Date().toLocaleTimeString());
const interval = setInterval(() => {
setTime(new Date().toLocaleTimeString());
}, 1000);
return () => clearInterval(interval);
}, []);
return <p>当前时间: {time ?? '加载中...'}</p>;
}
// 服务端渲染: "当前时间: 加载中..."
// 客户端Hydrate为相同内容: "当前时间: 加载中..."
// useEffect更新为真实时间(无匹配问题)Understanding the Hydration Performance Cost
理解Hydration的性能开销
Hydration is expensive. Let's understand why:
javascript
// For a page with 1000 DOM nodes:
HYDRATION WORK:
1. Download JS bundle (network time)
2. Parse JavaScript (CPU time)
3. Execute all component code (CPU time)
4. Build virtual DOM tree (memory + CPU)
5. Walk real DOM tree (CPU time)
6. Compare virtual vs real (CPU time)
7. Attach all event listeners (memory)
8. Initialize all state (memory)
// For 1000 nodes, this might take 200-500ms on mobile
// During this time, clicks don't work!Measuring hydration cost:
javascript
// React 18 provides timing
const startMark = performance.now();
hydrateRoot(container, <App />);
const endMark = performance.now();
console.log(`Hydration took ${endMark - startMark}ms`);
// More detailed with React Profiler
<Profiler id="App" onRender={(id, phase, duration) => {
if (phase === 'mount') {
// This includes hydration time
console.log(`${id} hydration: ${duration}ms`);
}
}}>
<App />
</Profiler>Hydration的开销很高,我们来分析原因:
javascript
// 对于包含1000个DOM节点的页面:
Hydration的工作量:
1. 下载JS包(网络耗时)
2. 解析JavaScript(CPU耗时)
3. 执行所有组件代码(CPU耗时)
4. 构建虚拟DOM树(内存+CPU耗时)
5. 遍历真实DOM树(CPU耗时)
6. 对比虚拟与真实DOM(CPU耗时)
7. 附加所有事件监听器(内存)
8. 初始化所有状态(内存)
// 对于1000个节点,在移动端可能需要200-500ms
// 在此期间,点击操作无响应!测量Hydration开销:
javascript
// React 18提供计时API
const startMark = performance.now();
hydrateRoot(container, <App />);
const endMark = performance.now();
console.log(`Hydration耗时 ${endMark - startMark}ms`);
// 使用React Profiler获得更详细数据
<Profiler id="App" onRender={(id, phase, duration) => {
if (phase === 'mount') {
// 包含Hydration耗时
console.log(`${id} Hydration耗时: ${duration}ms`);
}
}}>
<App />
</Profiler>Islands Architecture: The Mental Model
岛屿架构:思维模型
Traditional hydration thinks in pages. Islands thinks in components:
TRADITIONAL (Page-Level Hydration):
┌──────────────────────────────────────────────┐
│ ┌──────────────────────────────────────┐ │
│ │ Page Component │ │
│ │ ┌──────┐ ┌──────┐ ┌──────────────┐ │ │
│ │ │Header│ │ Nav │ │ Content │ │ │
│ │ └──────┘ └──────┘ └──────────────┘ │ │
│ │ ┌──────┐ ┌──────────────────────┐ │ │
│ │ │Button│ │ Comments │ │ │
│ │ └──────┘ └──────────────────────┘ │ │
│ └──────────────────────────────────────┘ │
│ │
│ ENTIRE TREE must be hydrated │
│ One root, all components connected │
└──────────────────────────────────────────────┘
ISLANDS (Component-Level Hydration):
┌──────────────────────────────────────────────┐
│ │
│ Static HTML (no JS required) │
│ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ Island: Nav │ │ Island: Search │ │
│ │ (own JS, own │ │ (own JS, own │ │
│ │ hydration) │ │ hydration) │ │
│ └──────────────┘ └──────────────────┘ │
│ │
│ More static HTML... │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Island: Comments Widget │ │
│ │ (loads only when visible) │ │
│ └──────────────────────────────────────┘ │
│ │
│ Each island is INDEPENDENT │
│ Hydrate separately, fail separately │
└──────────────────────────────────────────────┘Islands technical implementation:
html
<!-- Astro compiles to something like this -->
<html>
<body>
<header>Static header content</header>
<!-- Island marker -->
<astro-island
component-url="/components/Counter.js"
client="visible"
>
<button>Count: 0</button>
</astro-island>
<main>Static main content</main>
<!-- Another island -->
<astro-island
component-url="/components/Comments.js"
client="idle"
props='{"postId":123}'
>
<div>Loading comments...</div>
</astro-island>
<footer>Static footer</footer>
</body>
</html>
<!-- The astro-island web component handles loading and hydration -->传统Hydration以页面为单位思考,岛屿架构以组件为单位:
传统(页面级Hydration):
┌──────────────────────────────────────────────┐
│ ┌──────────────────────────────────────┐ │
│ │ 页面组件 │ │
│ │ ┌──────┐ ┌──────┐ ┌──────────────┐ │ │
│ │ │头部│ │ 导航 │ │ 内容区 │ │ │
│ │ └──────┘ └──────┘ └──────────────┘ │ │
│ │ ┌──────┐ ┌──────────────────────┐ │ │
│ │ │按钮│ │ 评论区 │ │ │
│ │ └──────┘ └──────────────────────┘ │ │
│ └──────────────────────────────────────┘ │
│ │
│ 整个组件树必须执行Hydration │
│ 单一根节点,所有组件相互关联 │
└──────────────────────────────────────────────┘
岛屿架构(组件级Hydration):
┌──────────────────────────────────────────────┐
│ │
│ 静态HTML(无需JS) │
│ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ 岛屿: 导航 │ │ 岛屿: 搜索框 │ │
│ │ (独立JS,独立 │ │ (独立JS,独立 │ │
│ │ Hydration) │ │ Hydration) │ │
│ └──────────────┘ └──────────────────┘ │
│ │
│ 更多静态HTML... │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ 岛屿: 评论组件 │ │
│ │ (仅可见时加载) │ │
│ └──────────────────────────────────────┘ │
│ │
│ 每个岛屿都是独立的 │
│ 独立执行Hydration,独立处理故障 │
└──────────────────────────────────────────────┘岛屿架构的技术实现:
html
<!-- Astro编译后类似如下结构 -->
<html>
<body>
<header>静态头部内容</header>
<!-- 岛屿标记 -->
<astro-island
component-url="/components/Counter.js"
client="visible"
>
<button>计数: 0</button>
</astro-island>
<main>静态主体内容</main>
<!-- 另一个岛屿 -->
<astro-island
component-url="/components/Comments.js"
client="idle"
props='{"postId":123}'
>
<div>加载评论中...</div>
</astro-island>
<footer>静态页脚</footer>
</body>
</html>
<!-- astro-island自定义元素负责加载和Hydration -->Resumability: How Qwik Eliminates Hydration
可恢复性:Qwik如何消除Hydration
Qwik takes a radically different approach. Instead of re-running code to figure out what event handlers should do, it serializes everything:
javascript
// TRADITIONAL HYDRATION:
// Server runs component, produces HTML
// Client runs SAME component code again to attach handlers
// Server:
function Counter() {
const [count, setCount] = useState(0);
const increment = () => setCount(c => c + 1);
return <button onClick={increment}>Count: {count}</button>;
}
// Output: <button>Count: 0</button>
// Client must run Counter() to know what onClick does
// QWIK RESUMABILITY:
// Server runs component, serializes EVERYTHING including handlers
// Server:
function Counter() {
const count = useSignal(0);
return <button onClick$={() => count.value++}>Count: {count.value}</button>;
}
// Output:
<button
on:click="counter_onclick_abc123.js#s0"
q:obj="0"
>Count: 0</button>
<script type="qwik/json">{"signals":{"0":0}}</script>
// Client sees: "When clicked, load counter_onclick_abc123.js and run s0"
// No need to run Counter() at all!
// Handler code downloaded ONLY when button is clickedWhy this matters:
HYDRATION TIMELINE:
[Page Loads] → [Download ALL JS] → [Execute ALL code] → [Interactive]
[====== Blocking, nothing works ======]
RESUMABILITY TIMELINE:
[Page Loads] → [Interactive immediately]
[Download handler only on first interaction]
// 100KB app with hydration: 500ms until interactive
// 100KB app with resumability: 0ms until interactive (JS loads on demand)Qwik采用了完全不同的方案。它不会重新运行代码来确定事件处理程序的逻辑,而是序列化所有内容:
javascript
// 传统HYDRATION:
// 服务端运行组件,生成HTML
// 客户端再次运行相同组件代码以附加处理程序
// 服务端:
function Counter() {
const [count, setCount] = useState(0);
const increment = () => setCount(c => c + 1);
return <button onClick={increment}>计数: {count}</button>;
}
// 输出: <button>计数: 0</button>
// 客户端必须运行Counter()才能知道onClick的逻辑
// QWIK可恢复性:
// 服务端运行组件,序列化所有内容包括处理程序
// 服务端:
function Counter() {
const count = useSignal(0);
return <button onClick$={() => count.value++}>计数: {count.value}</button>;
}
// 输出:
<button
on:click="counter_onclick_abc123.js#s0"
q:obj="0"
>计数: 0</button>
<script type="qwik/json">{"signals":{"0":0}}</script>
// 客户端看到: "点击时,加载counter_onclick_abc123.js并运行s0"
// 完全无需运行Counter()!
// 处理程序代码仅在按钮首次点击时下载这一方案的意义:
HYDRATION时间线:
[页面加载] → [下载所有JS] → [执行所有代码] → [可交互]
[====== 阻塞期,无任何响应 ======]
可恢复性时间线:
[页面加载] → [立即可交互]
[仅在首次交互时下载处理程序]
// 100KB的Hydration应用: 500ms后才可交互
// 100KB的可恢复性应用: 0ms即可交互(JS按需加载)Partial Hydration: The Compiler's Role
部分水合:编译器的作用
Frameworks like Astro use compilers to determine what needs JavaScript:
javascript
// Source code
---
import Header from './Header.astro'; // Astro component (static)
import Counter from './Counter.jsx'; // React component (interactive)
---
<Header />
<Counter client:load />
// COMPILER ANALYSIS:
// Header.astro:
// - No useState, no useEffect, no event handlers
// - Only template logic
// - RESULT: Compile to pure HTML, ship NO JavaScript
// Counter.jsx:
// - Has useState, has onClick
// - Needs interactivity
// - RESULT: Ship JavaScript, hydrate this component only
// BUILD OUTPUT:
// index.html: Full HTML with Header content + Counter placeholder
// counter.[hash].js: Only Counter component code (~2KB)
// NOT included: React runtime for static components像Astro这样的框架使用编译器来确定哪些内容需要JavaScript:
javascript
// 源代码
---
import Header from './Header.astro'; // Astro组件(静态)
import Counter from './Counter.jsx'; // React组件(交互式)
---
<Header />
<Counter client:load />
// 编译器分析:
// Header.astro:
// - 无useState、无useEffect、无事件处理程序
// - 仅模板逻辑
// - 结果: 编译为纯HTML,不打包任何JavaScript
// Counter.jsx:
// - 包含useState、onClick
// - 需要交互性
// - 结果: 打包JavaScript,仅对该组件执行Hydration
// 构建输出:
// index.html: 包含Header内容和Counter占位符的完整HTML
// counter.[hash].js: 仅Counter组件代码(约2KB)
// 不包含: 静态组件的React运行时The Event Listener Memory Model
事件监听器的内存模型
Understanding how event listeners work clarifies hydration cost:
javascript
// Without framework (vanilla JS):
button.addEventListener('click', () => console.log('clicked'));
// Browser stores: {element: button, event: 'click', handler: fn}
// Memory: ~100 bytes per listener
// With React (synthetic events):
<button onClick={() => console.log('clicked')}>
// React stores in its own system:
// - Element reference
// - Event type
// - Handler function
// - Handler closure scope (variables it captures)
// - Fiber node reference
// - Event priority
// Memory: ~500 bytes per listener + closure scope
// A page with 100 interactive elements:
// Vanilla: ~10KB in event system
// React: ~50KB+ in React's event system + component treeThis is why hydration is expensive - it's rebuilding all these data structures.
理解事件监听器的工作原理有助于明确Hydration的开销:
javascript
// 无框架(原生JS):
button.addEventListener('click', () => console.log('点击了'));
// 浏览器存储: {element: button, event: 'click', handler: fn}
// 内存: 每个监听器约100字节
// 使用React(合成事件):
<button onClick={() => console.log('点击了')}>
// React在自身系统中存储:
// - 元素引用
// - 事件类型
// - 处理程序函数
// - 处理程序의闭包作用域(捕获的变量)
// - Fiber节点引用
// - 事件优先级
// 内存: 每个监听器约500字节 + 闭包作用域
// 包含100个交互式元素的页面:
// 原生JS: 事件系统约占用10KB
// React: React事件系统+组件树约占用50KB+这就是Hydration开销高的原因——它需要重建所有这些数据结构。
Streaming Hydration: Out of Order
流式Hydration:无序执行
React 18's streaming SSR allows components to hydrate out of order:
jsx
<Layout>
<Header /> {/* Hydrates first */}
<Suspense fallback={<Spinner />}>
<SlowComments /> {/* Hydrates when ready, even if Footer is waiting */}
</Suspense>
<Suspense fallback={<Spinner />}>
<SlowSidebar /> {/* Can hydrate before or after Comments */}
</Suspense>
<Footer /> {/* Hydrates independently */}
</Layout>The streaming mechanism:
1. Server streams shell immediately:
<html><body><div id="header">...</div><div id="comments">Loading...</div>
2. As data arrives, server streams script tags:
<script>
$RC('comments', '<div class="comments">Real comments...</div>');
</script>
3. $RC (React's internal function) swaps content and triggers hydration
for just that subtree
4. User can interact with Header while Comments still loadingReact 18的流式SSR允许组件无序执行Hydration:
jsx
<Layout>
<Header /> {/* 首先执行Hydration */}
<Suspense fallback={<Spinner />}>
<SlowComments /> {/* 准备就绪后执行Hydration,即使页脚还在等待 */}
</Suspense>
<Suspense fallback={<Spinner />}>
<SlowSidebar /> {/* 可在评论区之前或之后执行Hydration */}
</Suspense>
<Footer /> {/* 独立执行Hydration */}
</Layout>流式机制:
1. 服务端立即流式传输外壳:
<html><body><div id="header">...</div><div id="comments">加载中...</div>
2. 数据到达时,服务端流式传输脚本标签:
<script>
$RC('comments', '<div class="comments">真实评论...</div>');
</script>
3. $RC(React内部函数)替换内容并触发该子树的Hydration
4. 用户在评论区加载期间即可与头部交互Real-World Hydration Optimization Strategies
实战Hydration优化策略
1. Code Splitting by Route:
javascript
// Don't bundle everything together
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
// Only Dashboard code loads on /dashboard
// Settings code only loads when navigating to /settings2. Deferred Hydration:
javascript
// Hydrate critical UI first
import { startTransition } from 'react';
// Critical path - hydrate immediately
hydrateRoot(headerContainer, <Header />);
// Non-critical - defer
startTransition(() => {
hydrateRoot(commentsContainer, <Comments />);
});3. Intersection Observer Pattern:
javascript
// Only hydrate when scrolled into view
function useHydrateOnVisible(ref) {
const [shouldHydrate, setShouldHydrate] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setShouldHydrate(true);
observer.disconnect();
}
},
{ rootMargin: '200px' } // Start 200px before visible
);
observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return shouldHydrate;
}4. Static Extraction:
javascript
// Identify components that need no JS
function ProductDescription({ text }) {
// No state, no effects, no handlers
// This could be static HTML
return <p className="description">{text}</p>;
}
// vs
function AddToCartButton({ productId }) {
const [loading, setLoading] = useState(false);
const handleClick = () => { /* ... */ };
// This NEEDS hydration
return <button onClick={handleClick}>Add to Cart</button>;
}1. 按路由拆分代码:
javascript
// 不要将所有内容打包在一起
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
// 仅在访问/dashboard时加载Dashboard代码
// 仅在导航到/settings时加载Settings代码2. 延迟Hydration:
javascript
// 优先水合关键UI
import { startTransition } from 'react';
// 关键路径 - 立即水合
hydrateRoot(headerContainer, <Header />);
// 非关键 - 延迟处理
startTransition(() => {
hydrateRoot(commentsContainer, <Comments />);
});3. 交叉观察器模式:
javascript
// 仅在滚动到视图中时执行水合
function useHydrateOnVisible(ref) {
const [shouldHydrate, setShouldHydrate] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setShouldHydrate(true);
observer.disconnect();
}
},
{ rootMargin: '200px' } // 提前200px开始
);
observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return shouldHydrate;
}4. 静态内容提取:
javascript
// 识别无需JS的组件
function ProductDescription({ text }) {
// 无状态、无副作用、无处理程序
// 可编译为静态HTML
return <p className="description">{text}</p>;
}
// 对比
function AddToCartButton({ productId }) {
const [loading, setLoading] = useState(false);
const handleClick = () => { /* ... */ };
// 需要Hydration
return <button onClick={handleClick}>加入购物车</button>;
}For Framework Authors: Building Hydration Systems
面向框架开发者:构建Hydration系统
Implementation Note: The patterns and code examples below represent one proven approach to building hydration systems. Hydration is one of the most complex areas in framework development with many valid strategies—from React's reconciliation-based approach to Qwik's resumability to Astro's islands. The direction shown here provides foundational concepts; adapt these patterns based on your framework's rendering model, state management, and performance priorities.
实现说明: 以下模式和代码示例代表了构建Hydration系统的一种成熟方案。Hydration是框架开发中最复杂的领域之一,存在多种有效策略——从React的基于协调的方案到Qwik的可恢复性,再到Astro的岛屿架构。以下内容提供了基础概念;请根据框架的渲染模型、状态管理和性能优先级调整这些模式。
Implementing Basic Hydration
实现基础Hydration
javascript
// MINIMAL HYDRATION IMPLEMENTATION
function hydrateRoot(container, element) {
// Get existing DOM nodes
const existingNodes = Array.from(container.childNodes);
// Walk virtual tree and existing DOM in parallel
hydrateNode(element, existingNodes[0], container);
}
function hydrateNode(vnode, domNode, parent) {
if (typeof vnode === 'string' || typeof vnode === 'number') {
// Text node - validate match
if (domNode?.nodeType === Node.TEXT_NODE) {
if (domNode.textContent !== String(vnode)) {
console.warn('Hydration mismatch:', domNode.textContent, '!=', vnode);
domNode.textContent = vnode;
}
}
return domNode;
}
if (!vnode) return null;
const { type, props } = vnode;
// Function component - call and hydrate result
if (typeof type === 'function') {
const result = type(props);
return hydrateNode(result, domNode, parent);
}
// Element node - validate and attach listeners
if (domNode?.nodeName?.toLowerCase() !== type) {
console.error('Hydration mismatch: expected', type, 'got', domNode?.nodeName);
// Full re-render fallback
const newNode = render(vnode);
parent.replaceChild(newNode, domNode);
return newNode;
}
// Attach event listeners (not present in SSR HTML)
attachEventListeners(domNode, props);
// Hydrate children
const children = Array.isArray(props.children)
? props.children
: props.children ? [props.children] : [];
const domChildren = Array.from(domNode.childNodes);
children.forEach((child, i) => {
hydrateNode(child, domChildren[i], domNode);
});
return domNode;
}
function attachEventListeners(element, props) {
Object.entries(props).forEach(([key, value]) => {
if (key.startsWith('on') && typeof value === 'function') {
const eventName = key.slice(2).toLowerCase();
element.addEventListener(eventName, value);
}
});
}javascript
// 极简HYDRATION实现
function hydrateRoot(container, element) {
// 获取现有DOM节点
const existingNodes = Array.from(container.childNodes);
// 并行遍历虚拟树和现有DOM
hydrateNode(element, existingNodes[0], container);
}
function hydrateNode(vnode, domNode, parent) {
if (typeof vnode === 'string' || typeof vnode === 'number') {
// 文本节点 - 验证匹配
if (domNode?.nodeType === Node.TEXT_NODE) {
if (domNode.textContent !== String(vnode)) {
console.warn('Hydration不匹配:', domNode.textContent, '!=', vnode);
domNode.textContent = vnode;
}
}
return domNode;
}
if (!vnode) return null;
const { type, props } = vnode;
// 函数组件 - 调用并水合结果
if (typeof type === 'function') {
const result = type(props);
return hydrateNode(result, domNode, parent);
}
// 元素节点 - 验证并附加监听器
if (domNode?.nodeName?.toLowerCase() !== type) {
console.error('Hydration不匹配: 预期', type, '实际', domNode?.nodeName);
// 回退到完全重新渲染
const newNode = render(vnode);
parent.replaceChild(newNode, domNode);
return newNode;
}
// 附加事件监听器(SSR HTML中不存在)
attachEventListeners(domNode, props);
// 水合子节点
const children = Array.isArray(props.children)
? props.children
: props.children ? [props.children] : [];
const domChildren = Array.from(domNode.childNodes);
children.forEach((child, i) => {
hydrateNode(child, domChildren[i], domNode);
});
return domNode;
}
function attachEventListeners(element, props) {
Object.entries(props).forEach(([key, value]) => {
if (key.startsWith('on') && typeof value === 'function') {
const eventName = key.slice(2).toLowerCase();
element.addEventListener(eventName, value);
}
});
}Building a Hydration Scheduler
构建Hydration调度器
javascript
// PROGRESSIVE HYDRATION SCHEDULER
class HydrationScheduler {
constructor() {
this.queue = [];
this.isHydrating = false;
this.idleDeadline = 50; // ms per frame
}
// Add component to hydration queue
schedule(component, priority = 'normal') {
this.queue.push({ component, priority });
this.sortQueue();
this.processQueue();
}
sortQueue() {
const priorityOrder = { critical: 0, high: 1, normal: 2, low: 3 };
this.queue.sort((a, b) =>
priorityOrder[a.priority] - priorityOrder[b.priority]
);
}
processQueue() {
if (this.isHydrating || this.queue.length === 0) return;
this.isHydrating = true;
requestIdleCallback((deadline) => {
while (
this.queue.length > 0 &&
(deadline.timeRemaining() > 0 || deadline.didTimeout)
) {
const { component } = this.queue.shift();
this.hydrateComponent(component);
}
this.isHydrating = false;
if (this.queue.length > 0) {
this.processQueue();
}
}, { timeout: 1000 });
}
hydrateComponent(component) {
const element = document.querySelector(
`[data-hydrate="${component.id}"]`
);
if (element) {
hydrateRoot(element, component.vnode);
}
}
}
// Visibility-based hydration trigger
class VisibilityHydration {
constructor(scheduler) {
this.scheduler = scheduler;
this.observer = new IntersectionObserver(
(entries) => this.onIntersect(entries),
{ rootMargin: '200px' }
);
}
observe(element, component) {
element.__component = component;
this.observer.observe(element);
}
onIntersect(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.observer.unobserve(entry.target);
this.scheduler.schedule(entry.target.__component, 'high');
}
});
}
}javascript
// 渐进式HYDRATION调度器
class HydrationScheduler {
constructor() {
this.queue = [];
this.isHydrating = false;
this.idleDeadline = 50; // 每帧耗时上限(毫秒)
}
// 将组件加入水合队列
schedule(component, priority = 'normal') {
this.queue.push({ component, priority });
this.sortQueue();
this.processQueue();
}
sortQueue() {
const priorityOrder = { critical: 0, high: 1, normal: 2, low: 3 };
this.queue.sort((a, b) =>
priorityOrder[a.priority] - priorityOrder[b.priority]
);
}
processQueue() {
if (this.isHydrating || this.queue.length === 0) return;
this.isHydrating = true;
requestIdleCallback((deadline) => {
while (
this.queue.length > 0 &&
(deadline.timeRemaining() > 0 || deadline.didTimeout)
) {
const { component } = this.queue.shift();
this.hydrateComponent(component);
}
this.isHydrating = false;
if (this.queue.length > 0) {
this.processQueue();
}
}, { timeout: 1000 });
}
hydrateComponent(component) {
const element = document.querySelector(
`[data-hydrate="${component.id}"]`
);
if (element) {
hydrateRoot(element, component.vnode);
}
}
}
// 基于可见性的水合触发器
class VisibilityHydration {
constructor(scheduler) {
this.scheduler = scheduler;
this.observer = new IntersectionObserver(
(entries) => this.onIntersect(entries),
{ rootMargin: '200px' }
);
}
observe(element, component) {
element.__component = component;
this.observer.observe(element);
}
onIntersect(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.observer.unobserve(entry.target);
this.scheduler.schedule(entry.target.__component, 'high');
}
});
}
}Implementing Resumability (Qwik-style)
实现可恢复性(Qwik风格)
javascript
// RESUMABILITY IMPLEMENTATION
// Server-side: Serialize component state and handlers
function serializeComponent(component, props) {
const handlers = {};
const state = {};
// Extract and serialize event handlers
Object.entries(props).forEach(([key, value]) => {
if (key.startsWith('on') && typeof value === 'function') {
// Serialize handler reference
const handlerId = generateHandlerId(value);
handlers[key] = handlerId;
// Store handler code location for lazy loading
registerHandler(handlerId, {
module: component.__module,
export: value.name,
});
} else {
state[key] = value;
}
});
return {
html: renderToString(component, props),
stateScript: `<script type="qwik/json">${JSON.stringify({
state,
handlers,
})}</script>`,
};
}
// Client-side: Resume without re-executing
function resume() {
// Parse serialized state
const stateScript = document.querySelector('script[type="qwik/json"]');
const { state, handlers } = JSON.parse(stateScript.textContent);
// Attach global event listener (event delegation)
document.addEventListener('click', async (event) => {
const target = event.target.closest('[on\\:click]');
if (!target) return;
const handlerId = target.getAttribute('on:click');
const handlerInfo = getHandler(handlerId);
// Lazy load handler module
const module = await import(handlerInfo.module);
const handler = module[handlerInfo.export];
// Execute with serialized state
handler(event, state);
});
}
// Emit resumable HTML
function renderResumable(component, props) {
const { html, stateScript } = serializeComponent(component, props);
return `
${html}
${stateScript}
<script src="/qwik-loader.js" async></script>
`;
}javascript
// 可恢复性实现
// 服务端: 序列化组件状态和处理程序
function serializeComponent(component, props) {
const handlers = {};
const state = {};
// 提取并序列化事件处理程序
Object.entries(props).forEach(([key, value]) => {
if (key.startsWith('on') && typeof value === 'function') {
// 序列化处理程序引用
const handlerId = generateHandlerId(value);
handlers[key] = handlerId;
// 存储处理程序代码位置用于懒加载
registerHandler(handlerId, {
module: component.__module,
export: value.name,
});
} else {
state[key] = value;
}
});
return {
html: renderToString(component, props),
stateScript: `<script type="qwik/json">${JSON.stringify({
state,
handlers,
})}</script>`,
};
}
// 客户端: 无需重新执行即可恢复
function resume() {
// 解析序列化的状态
const stateScript = document.querySelector('script[type="qwik/json"]');
const { state, handlers } = JSON.parse(stateScript.textContent);
// 附加全局事件监听器(事件委托)
document.addEventListener('click', async (event) => {
const target = event.target.closest('[on\\:click]');
if (!target) return;
const handlerId = target.getAttribute('on:click');
const handlerInfo = getHandler(handlerId);
// 懒加载处理程序模块
const module = await import(handlerInfo.module);
const handler = module[handlerInfo.export];
// 使用序列化状态执行
handler(event, state);
});
}
// 生成可恢复的HTML
function renderResumable(component, props) {
const { html, stateScript } = serializeComponent(component, props);
return `
${html}
${stateScript}
<script src="/qwik-loader.js" async></script>
`;
}Building Islands Architecture
构建岛屿架构
javascript
// ISLANDS ARCHITECTURE IMPLEMENTATION
// Build-time: Analyze component for interactivity
function analyzeComponent(component, ast) {
return {
hasState: astContains(ast, 'useState', 'useSignal', 'createSignal'),
hasEffects: astContains(ast, 'useEffect', 'onMount'),
hasHandlers: astContains(ast, /^on[A-Z]/),
imports: extractImports(ast),
};
}
// Build-time: Mark islands in HTML
function markIslands(html, components) {
return html.replace(
/<([A-Z]\w+)([^>]*)client:(\w+)([^>]*)>/g,
(match, name, before, strategy, after) => {
const component = components[name];
const props = extractProps(before + after);
return `<astro-island
component-url="${component.url}"
component-export="${component.export}"
renderer-url="${component.renderer}"
props="${encodeProps(props)}"
client="${strategy}"
${before}${after}>`;
}
);
}
// Client-side: Island loader
class AstroIsland extends HTMLElement {
async connectedCallback() {
const strategy = this.getAttribute('client');
switch (strategy) {
case 'load':
await this.hydrate();
break;
case 'idle':
if ('requestIdleCallback' in window) {
requestIdleCallback(() => this.hydrate());
} else {
setTimeout(() => this.hydrate(), 200);
}
break;
case 'visible':
const observer = new IntersectionObserver(async ([entry]) => {
if (entry.isIntersecting) {
observer.disconnect();
await this.hydrate();
}
});
observer.observe(this);
break;
case 'media':
const query = this.getAttribute('client-media');
const mq = window.matchMedia(query);
if (mq.matches) {
await this.hydrate();
} else {
mq.addEventListener('change', () => this.hydrate(), { once: true });
}
break;
}
}
async hydrate() {
// Load component and renderer
const [Component, { default: render }] = await Promise.all([
import(this.getAttribute('component-url')),
import(this.getAttribute('renderer-url')),
]);
const props = JSON.parse(
decodeURIComponent(this.getAttribute('props'))
);
const exportName = this.getAttribute('component-export');
// Hydrate with framework-specific renderer
await render(
this,
Component[exportName],
props,
this.innerHTML
);
}
}
customElements.define('astro-island', AstroIsland);javascript
// 岛屿架构实现
// 构建时: 分析组件的交互性
function analyzeComponent(component, ast) {
return {
hasState: astContains(ast, 'useState', 'useSignal', 'createSignal'),
hasEffects: astContains(ast, 'useEffect', 'onMount'),
hasHandlers: astContains(ast, /^on[A-Z]/),
imports: extractImports(ast),
};
}
// 构建时: 在HTML中标记岛屿
function markIslands(html, components) {
return html.replace(
/<([A-Z]\w+)([^>]*)client:(\w+)([^>]*)>/g,
(match, name, before, strategy, after) => {
const component = components[name];
const props = extractProps(before + after);
return `<astro-island
component-url="${component.url}"
component-export="${component.export}"
renderer-url="${component.renderer}"
props="${encodeProps(props)}"
client="${strategy}"
${before}${after}>`;
}
);
}
// 客户端: 岛屿加载器
class AstroIsland extends HTMLElement {
async connectedCallback() {
const strategy = this.getAttribute('client');
switch (strategy) {
case 'load':
await this.hydrate();
break;
case 'idle':
if ('requestIdleCallback' in window) {
requestIdleCallback(() => this.hydrate());
} else {
setTimeout(() => this.hydrate(), 200);
}
break;
case 'visible':
const observer = new IntersectionObserver(async ([entry]) => {
if (entry.isIntersecting) {
observer.disconnect();
await this.hydrate();
}
});
observer.observe(this);
break;
case 'media':
const query = this.getAttribute('client-media');
const mq = window.matchMedia(query);
if (mq.matches) {
await this.hydrate();
} else {
mq.addEventListener('change', () => this.hydrate(), { once: true });
}
break;
}
}
async hydrate() {
// 加载组件和渲染器
const [Component, { default: render }] = await Promise.all([
import(this.getAttribute('component-url')),
import(this.getAttribute('renderer-url')),
]);
const props = JSON.parse(
decodeURIComponent(this.getAttribute('props'))
);
const exportName = this.getAttribute('component-export');
// 使用框架专属渲染器执行水合
await render(
this,
Component[exportName],
props,
this.innerHTML
);
}
}
customElements.define('astro-island', AstroIsland);State Serialization for Hydration
Hydration的状态序列化
javascript
// STATE SERIALIZATION SYSTEM
class StateSerializer {
constructor() {
this.stateMap = new Map();
this.counter = 0;
}
// During SSR: capture state
captureState(componentId, state) {
this.stateMap.set(componentId, this.serialize(state));
}
serialize(value) {
if (value === null || value === undefined) return value;
if (typeof value === 'function') return undefined; // Can't serialize
if (value instanceof Date) return { __type: 'Date', value: value.toISOString() };
if (value instanceof Map) return { __type: 'Map', value: Array.from(value) };
if (value instanceof Set) return { __type: 'Set', value: Array.from(value) };
if (ArrayBuffer.isView(value)) {
return { __type: 'TypedArray', ctor: value.constructor.name, value: Array.from(value) };
}
if (Array.isArray(value)) return value.map(v => this.serialize(v));
if (typeof value === 'object') {
const result = {};
for (const [k, v] of Object.entries(value)) {
result[k] = this.serialize(v);
}
return result;
}
return value;
}
// Emit script tag with serialized state
emit() {
const data = Object.fromEntries(this.stateMap);
return `<script id="__HYDRATION_STATE__" type="application/json">${
JSON.stringify(data).replace(/</g, '\\u003c')
}</script>`;
}
}
// Client-side: restore state
function restoreState() {
const script = document.getElementById('__HYDRATION_STATE__');
if (!script) return new Map();
const data = JSON.parse(script.textContent);
const stateMap = new Map();
for (const [id, serialized] of Object.entries(data)) {
stateMap.set(id, deserialize(serialized));
}
return stateMap;
}
function deserialize(value) {
if (value === null || value === undefined) return value;
if (value?.__type === 'Date') return new Date(value.value);
if (value?.__type === 'Map') return new Map(value.value);
if (value?.__type === 'Set') return new Set(value.value);
if (value?.__type === 'TypedArray') {
const Ctor = globalThis[value.ctor];
return new Ctor(value.value);
}
if (Array.isArray(value)) return value.map(deserialize);
if (typeof value === 'object') {
const result = {};
for (const [k, v] of Object.entries(value)) {
result[k] = deserialize(v);
}
return result;
}
return value;
}javascript
// 状态序列化系统
class StateSerializer {
constructor() {
this.stateMap = new Map();
this.counter = 0;
}
// SSR期间: 捕获状态
captureState(componentId, state) {
this.stateMap.set(componentId, this.serialize(state));
}
serialize(value) {
if (value === null || value === undefined) return value;
if (typeof value === 'function') return undefined; // 无法序列化
if (value instanceof Date) return { __type: 'Date', value: value.toISOString() };
if (value instanceof Map) return { __type: 'Map', value: Array.from(value) };
if (value instanceof Set) return { __type: 'Set', value: Array.from(value) };
if (ArrayBuffer.isView(value)) {
return { __type: 'TypedArray', ctor: value.constructor.name, value: Array.from(value) };
}
if (Array.isArray(value)) return value.map(v => this.serialize(v));
if (typeof value === 'object') {
const result = {};
for (const [k, v] of Object.entries(value)) {
result[k] = this.serialize(v);
}
return result;
}
return value;
}
// 生成包含序列化状态的脚本标签
emit() {
const data = Object.fromEntries(this.stateMap);
return `<script id="__HYDRATION_STATE__" type="application/json">${
JSON.stringify(data).replace(/</g, '\\u003c')
}</script>`;
}
}
// 客户端: 恢复状态
function restoreState() {
const script = document.getElementById('__HYDRATION_STATE__');
if (!script) return new Map();
const data = JSON.parse(script.textContent);
const stateMap = new Map();
for (const [id, serialized] of Object.entries(data)) {
stateMap.set(id, deserialize(serialized));
}
return stateMap;
}
function deserialize(value) {
if (value === null || value === undefined) return value;
if (value?.__type === 'Date') return new Date(value.value);
if (value?.__type === 'Map') return new Map(value.value);
if (value?.__type === 'Set') return new Set(value.value);
if (value?.__type === 'TypedArray') {
const Ctor = globalThis[value.ctor];
return new Ctor(value.value);
}
if (Array.isArray(value)) return value.map(deserialize);
if (typeof value === 'object') {
const result = {};
for (const [k, v] of Object.entries(value)) {
result[k] = deserialize(v);
}
return result;
}
return value;
}Related Skills
相关技能
- See rendering-patterns for SSR/SSG context
- See web-app-architectures for SPA vs MPA
- See meta-frameworks-overview for framework hydration strategies
- 查看 rendering-patterns 了解SSR/SSG相关内容
- 查看 web-app-architectures 了解SPA vs MPA
- 查看 meta-frameworks-overview 了解框架的Hydration策略