Hydration 失败源于服务端 HTML 与客户端 VNode 树节点不一致。核心要求是逐节点结构匹配,常见原因包括 table 缺失 thead/tbody、条件渲染分支不一致、非法 HTML 嵌套、客户端副作用提前执行等。

Hydration 失败不是客户端没跑起来,而是它“认不出”服务端吐出的 HTML —— 节点对不上,直接放弃复用,清空重 render。核心就一条:服务端生成的 DOM 树,必须和客户端首次 render 出的 VNode 树逐节点一致。
table 元素缺少 <thead>/<tbody> 导致 mismatch
Next.js 或 React SSR 下,<table> 里直接写 <tr> 是常见雷区。服务端按 JSX 原样输出,但浏览器解析时会自动补 <tbody>,客户端 VNode 却没这层,结构立刻错位。
- 必须显式包裹:
<thead>和<tbody>都不能省,哪怕其中一个是空的 -
<tfoot>可选,但若使用,得放在<tbody>之前(HTML 规范强制顺序) - 别用
<div class="table">模拟表格——可访问性、SEO、hydration 全崩
条件渲染分支在服务端/客户端不一致
v-if / && / ternary 在两端产出不同 DOM,是最隐蔽的 mismatch 来源。比如服务端没 token 渲染登录框,客户端 localStorage 有 token 就渲染首页,整个子树结构完全错开。
- 检查所有影响渲染的变量是否来自服务端预取数据(如
useAsyncData、getServerSideProps),而非客户端临时读取localStorage或cookie - 避免在
setup()或data()中直接访问window或document—— 服务端会报错或返回undefined,客户端却正常执行 - 时间类值(
new Date().toISOString()、performance.now())和服务端时间必须对齐,否则 class 或文本内容不一致
HTML 嵌套违反规范引发隐式修正
浏览器对非法嵌套会做“容错修复”,但服务端不会。比如 <p> 里放 <div>、<ul> 或另一个 <p>,服务端原样输出,客户端却被浏览器塞进匿名 <div> 或移出父容器,hydration 立刻失败。
将调研报告、方案对比、数据分析、需求梳理、会议纪要、设计说明、代码评审、学习笔记等任务产物输出为可在浏览器直接打开的单文件 HTML,替代 Markdown 作为最终交付物。Markdown 在信息密度、视觉清晰度和可分享性上已跟不上 AI 产物的丰富度,HTML 能同时承载排版、表格、SVG 等。
立即学习“前端免费学习笔记(深入)”;
- 用浏览器 Elements 面板对比服务端 HTML(查看页面源代码)和客户端实际 DOM 结构,重点看顶层容器和嵌套层级
- 禁用 JS 后刷新页面,观察纯 HTML 是否仍能正确显示语义结构;如果错乱,说明 HTML 本身就不合法
- 表单相关尤其敏感:
<form>必须闭合完整,<button type="submit">必须在<form>内部,否则 submit 事件根本不会触发,hydration 也无从谈起
客户端副作用提前执行破坏初始结构
像 onMounted 里操作 DOM、动态插入节点、修改 innerHTML,或者第三方 SDK(如 analytics、chat widget)在 hydration 完成前就改了页面,都会让 React/Vue 的节点匹配逻辑失效。
- 所有 DOM 操作、尺寸测量、第三方初始化,必须用
if (typeof window !== 'undefined')包裹,或明确放在onMounted/useEffect(() => {}, [])中 - 避免在组件顶层(非生命周期钩子内)直接调用
document.querySelector或el.scrollIntoView - keep-alive 组件或异步加载组件(
dynamic(..., { ssr: false }))要确保服务端跳过时,客户端也跳过,否则 slot 内容或包裹层级必然不一致
真正难定位的 hydration 错误,往往不报具体哪一行,只说 “mismatch at <div>”。这时候别猜逻辑,先比对服务端 HTML 和客户端最终 DOM 结构 —— 差异通常藏在最基础的 HTML 合法性或环境判断上。


















