跳到主要内容

SSR 与 Hydration

Vue 3 的 SSR 讨论,已经不只是“能不能服务端渲染”这么简单了。更实际的问题是:补水稳不稳、错位好不好查、哪些内容适合延后补水、哪些地方最容易在真实项目里出问题。

SSR 和 Hydration 不是一回事

  • SSR:服务端先输出 HTML
  • Hydration:客户端拿到 HTML 后,把这棵静态树接成真正可交互的应用

如果服务端输出和客户端首次渲染结果不一致,就会出现 Hydration mismatch。

常见 mismatch 来源

1. 时间和随机数

const now = Date.now()
const id = Math.random()

服务端和客户端执行时机不同,结果几乎一定不同。

2. 直接读浏览器环境

  • window
  • document
  • localStorage
  • navigator

这些只在浏览器存在。服务端渲染阶段直接用,很容易出问题。

3. 条件分支依赖客户端状态

例如根据窗口宽度、用户首屏缓存、浏览器主题来决定模板结构。如果服务端和客户端条件不一致,补水就会错位。

Vue 3.4 之后,这块为什么更值得补

3.4 开始,Hydration mismatch 的提示更明确。排错体验比早期版本好得多。

3.5 又继续往前推了两件事:

  • useId():服务端和客户端生成一致 ID
  • data-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,而在于:

  • 首屏结构是否稳定
  • 补水是否可预测
  • 错位时是否容易定位
  • 延后补水是不是有明确收益