根本原因是浏览器自动补全 dom 结构,和服务端输出不一致:服务端省略 时,浏览器强制插入导致客户端真实 dom 与 react 虚拟 dom 结构不匹配,触发 hydration mismatch。

为什么 SSR 渲染的 <table> 会触发 hydration mismatch<p>根本原因是浏览器自动补全 DOM 结构,和服务端输出不一致。比如服务端返回 <code><table><tr><td>A</td></tr></table>,客户端 React 水合时发现真实 DOM 多了一个自动生成的 <tbody>,而虚拟 DOM 没有——结构对不上,React 就丢弃服务端 HTML,重新渲染整张表。<p>这不是 React 的 bug,是 HTML 规范强制要求:<code><tr> 必须嵌套在 <code><thead>、<code><tbody> 或 <code><tfoot> 内。漏写 <code><tbody> 是非法 HTML,浏览器解析时会“修复”,但修复方式不可控(有的补在开头,有的补在结尾)。<ul>
<li>Next.js、Remix、Nuxt 等 SSR 框架都受此影响,尤其在 <code>use client 组件中直接写 table 时高频出现
Warning: Expected server HTML to contain a matching <tbody> in <table>
<li>实际后果不只是警告:首次交互延迟、滚动位置丢失、焦点错乱、CSS 选择器失效(如 <code>table tbody tr:first-child)怎么写 table 才能绕过 hydration mismatch
唯一可靠做法:显式写出所有合法父容器。哪怕内容空,也要写 <tbody>,且不能靠 JS 动态插入或 CSS 生成。<p>Next.js 示例(必须加 <code>"use client"):
"use client";
export default function TableExample() {
return (
| Company | Contact | Country |
|---|---|---|
| Alfreds Futterkiste | Maria Anders | Germany |
<thead> 和 <code><tbody> 都要手动写,哪怕只有一组数据<li>不要用 <code>dangerouslySetInnerHTML或[innerHTML]注入 table 片段——Angular/Next.js 中这会导致双重渲染- 如果数据动态生成,确保每次 render 都保持相同结构层级,避免条件渲染导致
<tbody> 消失<h3>客户端 fallback 渲染:当 hydration 真失败了怎么办</h3> <p>即使结构合法,网络抖动、资源加载延迟、组件状态不一致仍可能导致 hydration 失败。这时不能让页面卡死或白屏,得降级为纯客户端渲染。</p><div class="aritcle_card flexRow artxards"> <div class="artcardd flexRow"> <a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill9263" title="Html2md"><img src="https://img.php.cn/upload/skill/000/000/081/179249945220127.jpg" alt="Html2md" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a> <div class="aritcle_card_info flexColumn"> <a rel="nofollow" href="/xiazai/skill9263" title="Html2md" class="overflowclass">Html2md</a> <p class="overflowclass">使用 Readability + Turndown 将 HTML 页面转换为干净的、Agent 友好的 Markdown。去除导航栏、广告、页脚、Cookie 横幅、社交按钮等干扰元素。支持 URL...</p> </div> <a rel="nofollow" href="/xiazai/skill9263" title="Html2md" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a> </div> </div> <p>关键不是“重试 hydration”,而是提前埋点、检测失败、切换渲染路径:</p> <ul> <li>用 <code>useEffect+useState标记 hydration 是否完成,初始设为false,成功后设为true - 在 hydration 失败回调里(如 Next.js 的
useEffect中捕获 error),设置isHydrated = false,触发重新 render - fallback 渲染逻辑应完全独立:不依赖服务端数据,用
useState+useSWR或fetch拉取数据,再画 table - 避免视觉跳变:给 table 容器加
transition: opacity 0.2s,hydration 成功时淡入,失败时淡出再淡入新内容
容易被忽略的细节:CSS 和 JS 选择器也依赖结构合法性
hydration mismatch 往往只是表象,背后常连带样式和脚本失效。比如你写了 table > tbody > tr:hover,但服务端没写 <tbody>,浏览器补完后,这个选择器在客户端才生效——服务端渲染的样式就“不生效”;JS 里用 <code>document.querySelector('table tbody') 在 hydration 前可能返回 null。
所以容灾不止是“让页面能显示”,更要保证行为一致:
- 所有 CSS 选择器必须基于显式写出的标签,别依赖浏览器自动补全
- DOM 查询逻辑(如
getElementsByClassName)要加存在性判断,别假设<tbody> 一定在<li>服务端和客户端使用同一份 CSS,禁用仅客户端生效的伪类(如 <code>:has()在 SSR 中不解析)
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!










