Hydration(水合)

SSR 场景下,客户端 JavaScript 接管服务端输出的静态 HTML、恢复交互能力的过程。各框架的水合策略差异巨大——从 React 的全量重执行到 Qwik 的 Resumability 接近零水合,水合成本是 SSR 性能的核心瓶颈。

核心概念

Hydration(水合)是服务端渲染(SSR)架构中客户端"接管"服务端输出 HTML 的过程。服务端先将组件树渲染为 HTML 字符串发送给浏览器(用户看到内容,即 FCP),随后客户端加载 JavaScript 并执行水合——将静态 HTML 转变为可交互的动态应用(达到 TTI)。

FCP 和 TTI 之间的时间差,就是水合的代价。

各框架的水合策略

React:全量水合 → 选择性水合

React 的 hydrateRoot 会遍历整棵组件树,重新执行每个组件函数,生成 VDOM 并与服务端 DOM 做比对(reconciliation)。所有 useState 初始值重新求值,所有 useEffect 重新注册——服务端执行了一遍,客户端又执行一遍,这就是"二次执行成本"。

React 18 引入了选择性水合(Selective Hydration):通过 <Suspense> 边界允许部分组件延迟水合,用户交互的区域优先水合。但核心问题未变——每个组件最终仍需完整重执行。

关键源码路径hydrateRoot → reconciler 的 tryToClaimNextHydratableInstance 负责将 Fiber 节点与已有 DOM 配对,配对失败则客户端重建整个子树。

Vue 3:属性级 Patch Flags 加速水合比对

Vue 的水合逻辑在 renderer.tshydrate 函数中。setup() 会重新执行以重建响应式系统(Proxy + effect 订阅),然后和服务端 DOM 做 patch 比对。Patch Flags 在水合路径上价值尤其大——hydrateElement 看到 PatchFlags.CLASS 标记就知道只需检查 class 属性,静态属性直接跳过,相当于编译器给水合管线"划重点"。

Vue 3.5+ 提供了 Lazy Hydration(defineAsyncComponent + hydrate 选项),支持组件级水合延迟。

Solid:轻量水合

Solid 的组件函数只执行一次。服务端执行一次生成 HTML,客户端水合时也只执行一次——但跳过 DOM 创建,只需将 Signal 的订阅关系接上已有 DOM 节点。本质上就是"绑事件监听器 + 恢复 Signal 订阅",无需 VDOM 比对。

注意:在非流式 SSR(renderToString)下,Solid 信任服务端 DOM 直接接管订阅,不做属性级比对——出现不一致时静默 bug 而非报错。流式模式(renderToStream)下,异步边界 resolve 后会校验水合标记(<!--#--> / <!--/-->)位置的一致性,标记不匹配时会抛错。

Qwik:Resumability(接近零水合)

Qwik 在哲学层面拒绝"水合"。编译器将每个事件处理函数拆成独立懒加载 chunk,服务端将组件状态和订阅图谱序列化到 HTML 属性(q: 前缀)。客户端加载时不执行组件函数,用户真正交互时才按需下载对应 chunk。

水合成本并非绝对为零——qwikloader.js(约 1KB)需在页面加载时同步执行以注册全局事件代理,通常耗时 <1ms。此外 prefetch 策略会在空闲时预加载可能被交互的 chunk,产生额外的网络请求和 JS 解析成本。但与传统水合相比,TTI 接近 FCP,首次交互存在 chunk 下载的 cold start 延迟。

→ 详见 Resumability 条目。

水合策略对比

框架水合策略客户端组件重执行TTI 代价
React (传统)全量水合全部重执行最高
React 18选择性水合全部重执行,但分批
React + RSC部分水合只重执行 Client Components
Vue / Nuxt全量 + 可选懒水合setup() 全部重执行中高
Solid轻量水合执行一次,接管订阅
QwikResumability不执行,按需加载接近零
Astro (Islands)岛屿水合只水合交互岛屿

水合成本的根源——响应式模型决定编译产物,编译产物决定水合难度

编译产物里是否包含 DOM 节点的精确路径信息,直接决定了水合需要做多少工作。Svelte/Solid 的编译器能生成 $.child(button) 这类精确 DOM 引用,水合时沿同一路径定位节点、接上 Signal 订阅即可。React 的编译产物只含 VDOM 描述(JSX),水合时必须重新执行组件函数生成 VDOM 再和 DOM 比对——这不是 React 编译器"不够好",而是 React 的渲染模型(无细粒度追踪)决定了编译器不可能知道"哪个状态影响哪个 DOM 节点"。

常见问题

Hydration Mismatch:服务端 HTML 与客户端 VDOM 不一致时触发。React 会在控制台报警告并以客户端为准重建,Vue 会 fallback 到客户端完整挂载。常见原因包括:使用 Date.now() / Math.random() 等在服务端和客户端产生不同值的 API(React 18 的 useId 专为解决此问题),以及浏览器自动修正不合法 HTML(如 <p> 内嵌套 <div>)。

Solid 在非流式模式下不做 DOM 比对,不一致时不报错但可能导致 Signal 绑定指向错误的 DOM 节点——React/Vue 选择"发现不一致就报警"(安全但噪音大),Solid 选择"信任服务端输出"(安静但出错时更难调试)。

引用本术语的文章