SSR 与 Hydration
Vue 3 的 SSR 讨论,已经不只是“能不能服务端渲染”这么简单了。更实际的问题是:补水稳不稳、错位好不好查、哪些内容适合延后补水、哪些地方最容易在真实项目里出问题。
SSR 和 Hydration 不是一回事
SSR:服务端先输出 HTMLHydration:客户端拿到 HTML 后,把这棵静态树接成真正可交互的应用
如果服务端输出和客户端首次渲染结果不一致,就会出现 Hydration mismatch。
常见 mismatch 来源
1. 时间和随机数
const now = Date.now()
const id = Math.random()
服务端和客户端执行时机不同,结果几乎一定不同。
2. 直接读浏览器环境
windowdocumentlocalStoragenavigator
这些只在浏览器存在。服务端渲染阶段直接用,很容易出问题。
3. 条件分支依赖客户端状态
例如根据窗口宽度、用户首屏缓存、浏览器主题来决定模板结构。如果服务端和客户端条件不一致,补水就会错位。
Vue 3.4 之后,这块为什么更值得补
3.4 开始,Hydration mismatch 的提示更明确。排错体验比早期版本好得多。
3.5 又继续往前推了两件事:
useId():服务端和客户端生成一致 IDdata-allow-mismatch:对一些可预期的差异做显式声明
useId()
useId() 适合解决“同一组件在 SSR 和 CSR 两端都需要稳定 ID”的问题。
典型场景是表单无障碍关联。
const inputId = useId()
这比手写随机字符串稳得多,尤其在服务端渲染页面里。
data-allow-mismatch
有些 mismatch 不是工程失误,而是业务本身就难做到完全一致。
例如:
- 用户本地时区时间显示
- 某些只在客户端补全的文案
- 极少量可接受的展示差异
这种情况下,可以用 data-allow-mismatch 显式标注,而不是让框架不停报警。
这不是“绕过错误”的开关,更像“这里的差异是已知的”。
Lazy Hydration
3.5 之后,Vue 在这条线上又往前走了一步。
某些异步组件不一定要首屏立刻补水,可以按进入视口、用户交互或者某种时机再补水。对内容型页面、信息量大的首页、首屏交互不重的区域,这很有价值。
Teleport defer
这也是 SSR 相关很实用的 一点。
弹窗、浮层、抽屉经常依赖页面后面才出现的挂载点。早期一旦目标节点时机不对,Teleport 容易出问题。defer 让挂载动作延后到当前渲染周期结束,更适合复杂页面结构。
更稳的实践方式
- 让首屏结构尽量稳定
- 把浏览器环境读取放进客户端时机里
- 随机值和时间值不要直接参与首屏结构生成
- 表单和无障碍 ID 优先用
useId() - 对已知差异,用显式方式标注,而不是默默忽略
判断标准
SSR 做得稳不稳,不在于页面能不能先吐出 HTML,而在于:
- 首屏结构是否稳定
- 补水是否可预测
- 错位时是否容易定位
- 延后补水是不是有明确收益