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

Hydration 失败不是客户端没跑起来,而是它“认不出”服务端吐出的 HTML —— 节点对不上,直接放弃复用,清空重 render。核心就一条:服务端生成的 DOM 树,必须和客户端首次 render 出的 VNode 树逐节点一致。
table 元素缺少 <thead>/<code><tbody> 导致 mismatch
<p>Next.js 或 React SSR 下,<code><table> 里直接写 <code><tr> 是常见雷区。服务端按 JSX 原样输出,但浏览器解析时会自动补 <code><tbody>,客户端 VNode 却没这层,结构立刻错位。
<ul><li>必须显式包裹:<code><thead> 和 <code><tbody> 都不能省,哪怕其中一个是空的
<li><code><tfoot> 可选,但若使用,得放在 <code><tbody> 之前(HTML 规范强制顺序)
<li>别用 <code><div class="table"> 模拟表格——可访问性、SEO、hydration 全崩
<h3>条件渲染分支在服务端/客户端不一致</h3>
<p>v-if / && / ternary 在两端产出不同 DOM,是最隐蔽的 mismatch 来源。比如服务端没 token 渲染登录框,客户端 localStorage 有 token 就渲染首页,整个子树结构完全错开。</p>
<ul>
<li>检查所有影响渲染的变量是否来自服务端预取数据(如 <code>useAsyncData、getServerSideProps),而非客户端临时读取 localStorage 或 cookie
setup() 或 data() 中直接访问 window 或 document —— 服务端会报错或返回 undefined,客户端却正常执行new Date().toISOString()、performance.now())和服务端时间必须对齐,否则 class 或文本内容不一致HTML 嵌套违反规范引发隐式修正
浏览器对非法嵌套会做“容错修复”,但服务端不会。比如 <p></p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill8961" title="html-to-pptx-converter"><img
src="https://img.php.cn/upload/skill/000/000/081/179232667384043.jpg" alt="html-to-pptx-converter" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill8961" title="html-to-pptx-converter" class="overflowclass">html-to-pptx-converter</a>
<p class="overflowclass">使用 PptxGenJS 和 Python 解析,将结构化的 HTML/CSS 幻灯片内容转换为原生、可编辑的 PowerPoint(.pptx)演示文稿。</p>
</div>
<a rel="nofollow" href="/xiazai/skill8961" title="html-to-pptx-converter" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div> 里放 <div>、<code><ul></ul> 或另一个 <p></p>,服务端原样输出,客户端却被浏览器塞进匿名 <div> 或移出父容器,hydration 立刻失败。
<ul>
<li>用浏览器 Elements 面板对比服务端 HTML(查看页面源代码)和客户端实际 DOM 结构,重点看顶层容器和嵌套层级</li>
<li>禁用 JS 后刷新页面,观察纯 HTML 是否仍能正确显示语义结构;如果错乱,说明 HTML 本身就不合法</li>
<li>表单相关尤其敏感:<code><form></form> 必须闭合完整,<button type="submit"></button> 必须在 <form></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 合法性或环境判断上。</div>










