data-server-rendered="true"必须显式写在根容器上,否则hydration跳过导致页面退化为纯csr;__initial_state__需准确读取还原,服务端客户端渲染结构须严格一致,注水时机须在dom ready后且校验通过。

data-server-rendered="true" 必须显式写在根容器上
漏掉这个属性,hydration 就直接跳过,页面退化成纯 CSR:点击没反应、v-model 不同步、表单输入秒丢。Vue 和 React 都靠它判断“这 DOM 是不是我服务端吐出来的”。
常见错误是只加在顶层 <div id="app">,但子组件没继承——别指望框架自动透传,它只认你手写的那个属性。
<ul>
<li>Vue SSR 默认由 <code>vue-server-renderer 注入,但如果你用自定义模板或拼接 HTML 字符串,必须手动加 data-server-rendered="true"
ReactDOMServer.renderToString() 不会自动加,得自己在根节点上硬编码,比如 <div id="root" data-server-rendered="true">...</div>
__INITIAL_STATE__ 要能被客户端准确读取并还原
注水前第一步不是挂载组件,而是从 <script id="__INITIAL_STATE__" type="application/json"></script> 里取数据。读错、解析错、还原错,后续所有 diff 都会失准,触发 Hydration mismatch。
典型失败场景:服务端序列化了 Date 或 Map,客户端 JSON.parse() 后变成 plain object,响应式失效;或者服务端写 state,客户端却去读 __INITIAL_DATA__。
- 统一用
JSON.parse(document.getElementById('__INITIAL_STATE__')?.textContent || '{}'),加空值 fallback - 还原后做类型校验,比如检查
user.id是否为number,避免服务端传字符串、客户端当数字用 - store 初始化逻辑里严禁出现
window或document,否则服务端执行报错,快照为空
服务端与客户端 render 输出结构必须严格一致
hydration 的比对逻辑是逐节点对比 VNode 树和真实 DOM。哪怕只是属性顺序不同、多一个空格、少一个注释、条件分支走错分支,都会导致 mismatch 并丢弃服务端 HTML。
这不是警告,是强制降级——你看到的“首屏快”,其实只是静态 HTML,交互能力全靠 JS 重绘,完全失去 SSR 意义。
- 确保服务端和客户端使用同一份组件代码,避免环境判断(如
process.env.NODE_ENV)导致渲染路径不一致 - 避免在 render 中使用非确定性值,比如
Math.random()、new Date().toISOString() - 服务端渲染时禁用依赖 DOM 的逻辑(如
getBoundingClientRect()),否则要么报错,要么输出和客户端不一致
注水时机不能早于 DOM ready 且不能跳过校验
客户端 JS 执行 hydrate 前,必须确认 DOM 已就绪、初始状态已还原、服务端标记存在。过早执行(比如在 DOMContentLoaded 前)会导致找不到根节点;跳过校验则可能把 mismatch 当正常流程继续。
Vue 的 createApp(...).mount('#app') 在 SSR 场景下实际是 hydrate,但前提是 data-server-rendered="true" 存在且状态还原完成;React 的 hydrateRoot 同理,若 DOM 不匹配会静默 fallback 到 createRoot。
- 不要在
里立即执行 hydration 逻辑,至少等document.getElementById('app')返回非 null - 检查
document.querySelector('[data-server-rendered="true"]')是否存在,不存在就别调hydrate - 开发环境开启
hydratation mismatch提示(Vue 的runtimeCompiler: true+ 开发模式,React 的React.StrictMode下 warning)











