09|水合与 SSR 性能
第七章 / 09。目标:把“HTML 已显示”和“组件已可交互”放到同一时间线上观察。
水合做了什么
服务端先生成 HTML;浏览器解析成 DOM 后,客户端用 createSSRApp() 创建同一应用,匹配已有 DOM 与 VNode,附加事件和组件状态。水合不是重新“显示一次 HTML”,也不是零成本:客户端仍需下载、解析和执行代码,再遍历需要接管的节点。
Vue 编译器的 Patch Flag 与 Block Tree 也能缩小水合遍历范围。但若服务端结构与客户端首轮输出不一致,Vue 需要警告并恢复,可能丢弃错误节点再挂载;错配既是正确性问题,也会增加工作。
用 mark 划出交互边界
以下是客户端入口的局部片段,需要已有 SSR HTML。第一次学到这里先读时间线,完成 Nuxt 最小运行环境 与水合实验后回访测量;不要放进普通 createApp 的 CSR 入口宣称测到了水合。
在客户端入口记录应用级标记:
import { createSSRApp, nextTick } from 'vue'
import App from './App.vue'
performance.mark('hydrate:start')
const app = createSSRApp(App)
app.mount('#app')
await nextTick()
performance.mark('hydrate:vue-committed')
await new Promise<void>(resolve => requestAnimationFrame(() => resolve()))
performance.mark('hydrate:next-frame')
performance.measure('hydrate:vue', 'hydrate:start', 'hydrate:vue-committed')
performance.measure('hydrate:to-frame', 'hydrate:start', 'hydrate:next-frame')
console.table(performance.getEntriesByType('measure'))这只测当前设备、构建、路由和一次运行。应在生产构建、固定页面和多轮采样中比较分布,并同时记录脚本体积、主线程长任务和真实交互;不能把 hydrate:vue 当作完整用户体验指标。
Vue 3.5 的延迟水合策略
异步组件可设置 hydrateOnIdle、hydrateOnVisible、hydrateOnMediaQuery 或 hydrateOnInteraction。它们推迟的是组件接管,不是服务端 HTML 生成:
import { defineAsyncComponent, hydrateOnInteraction } from 'vue'
const Reviews = defineAsyncComponent({
loader: () => import('./Reviews.vue'),
hydrate: hydrateOnInteraction('click')
})interaction 策略会在水合后重放触发事件,适合已有可见内容、首次点击可容忍加载的区域。首屏搜索框、立即可用导航等关键交互不应随意延迟。visible 依赖 IntersectionObserver,idle 依赖宿主调度;都要提供版本与兼容策略。
错配与边界
随机数、时区、本地存储和无效 HTML 嵌套都会让两端首轮输出不同。优先共享种子/序列化状态,或把真正只属于客户端的显示延后到 mounted。Vue 3.5 的 data-allow-mismatch 只用于不可避免且已理解的差异;它压制警告,不修复数据流,也不应覆盖整棵树。
跨请求状态隔离和敏感序列化属于 SSR 安全合同,将在下一站展开;性能优化不能牺牲这条边界。
练习:选择水合时机
为首屏搜索框、视口下方评论区、仅桌面显示的图表、静态版权信息分别选择“立即、visible、media query、无需客户端脚本”之一。写出触发信号、用户首次操作、失败回退和一条可测指标;其中搜索框必须解释为何不能只看 LCP。
参考答案:第 7 章第 09 题。