返回首页

Nuxt的水合

布莱克2026-05-25 17:49已编辑
Tip:文章封面与内容无关,作者旅游时拍摄,因为没什么值得把四季都错过!

什么是水合?

SSR 的静态 HTML 变成交互应用的过程

水合的作用?

恢复交互能力:

// 服务端返回的只是死 HTML
<div>
  <button>点击我</button>  // ❌ 点了没反应
</div>

// 水合后变成活应用
<div>
  <button @click="handleClick">点击我</button>  // ✅ 可以交互
</div>

接管响应式数据:

// 水合前:显示初始计数 0
<button>0</button>

// 水合后:Vue/React 接管数据,点击可以增加
const count = ref(0)  // 恢复到 0
<button @click="count++">{{ count }}</button>

同步组件状态:

// 服务端创建了某个状态
<div class="user-login">欢迎, 张三</div>

// 水合时恢复对应的 JS 状态
const user = ref({ name: '张三' })  // 恢复状态
// 后续操作基于这个状态继续


Nuxt的渲染流程

Nuxt 完整保留了 Vue 的所有生命周期钩子(如 beforeCreatemounted 等),但关键在于它们运行的环境不同

  • 服务端:beforeCreate 和 created 会在 Node.js 环境中执行。这意味着在这里不能使用 window、document 等浏览器特有的 API。
  • 客户端:beforeMount 和 mounted 只在浏览器中执行,因此需要操作 DOM 或访问浏览器 API 的代码,都应该放在这里。从 mounted 开始,之后的钩子就只运行在客户端了。

Nuxt 特有的生命周期钩子(在服务端运行):

钩子运行环境主要作用
nuxtServerInit服务端在服务端渲染前,用于初始化 Vuex 状态,例如将服务端获取的用户信息存入 store。
middleware服务端/客户端在进入页面或布局之前执行,常用于路由验证、重定向等。
validate()服务端校验动态路由参数是否有效,若无效可返回 404 页面。
asyncData服务端/客户端最重要的钩子之一,用于在页面组件加载前异步获取数据,并将返回值合并到组件的数据中。它在服务端执行后,会在客户端水合前再次执行。
fetch服务端/客户端用于在页面渲染前填充 Vuex store 的状态。


渲染流程及水合位置:

第一步:浏览器请求 → Node.js 服务端

当用户首次访问你的 Nuxt 应用时,请求会先到达 Node.js 服务器。服务器会执行一系列操作,包括运行nuxtServerInitmiddleware、以及页面组件中的asyncDatafetch函数来获取所有需要的数据。

第二步:服务端 → 完全静态的 HTML

服务器利用获取到的数据,将 Vue 组件“渲染”成一个完整的 HTML 字符串,然后作为响应发送回浏览器。这时,用户能立即看到页面的完整内容,这就是 SSR 带来的首屏加载优势。

第三步:浏览器接收 → 水合 (Hydration)

  1. 加载 JS:浏览器接收到 HTML 后,会继续下载页面所需的 JavaScript 文件。
  2. 执行水合 (Hydration):这是最关键的一步。当所有 JS 加载完毕,Vue 会接管页面的静态 HTML。它会在后台重新执行一遍组件逻辑(比如重新初始化 data),并将事件监听器绑定到已有的 HTML 元素上。
  3. 应用激活:水合完成后,原本静态的 HTML 就变成了一个动态的、可交互的 Vue 应用。页面上的按钮可以点击了,数据也可以响应式更新了。

水合是连接“服务端渲染的静态页面”和“客户端动态应用”的桥梁,它发生在浏览器下载并执行完所有必要的 JS 代码之后


水合过程中,最大的坑就是服务端生成的 HTML 和客户端 Vue 期望生成的 HTML 结构不一致。这会导致 Vue 被迫放弃服务端渲染的 DOM 树,重新在客户端渲染,引发性能问题、交互失效,甚至页面闪烁


为什么需要延迟水合?

全量水合的问题:

// 传统方式:所有组件一起水合
页面加载 (0ms)
  ↓
下载所有 JS (300ms)
  ↓
同时水合 7 个组件 (500ms)  ← 主线程完全阻塞
  ↓
页面可交互 (800ms)  ❌ 用户需要等待 800ms 才能点击按钮
  1. 主线程阻塞:水合是 CPU 密集型操作,全部同时做会卡死页面
  2. 浪费资源:用户可能根本看不到页脚,却要为它执行水合
  3. TTI 时间长:Time to Interactive 很慢,用户以为页面卡死了
  4. 移动端更糟:低端设备水合比如 7 个组件可能需要 2-3 秒

水合过程:

// 服务端返回的静态 HTML
<div id="app">
  <button class="counter">0</button>
</div>

// 水合过程(简化版)
function hydrate() {
  // 1. 遍历整个 DOM 树(CPU 密集)
  const buttons = document.querySelectorAll('button')
  
  // 2. 为每个元素绑定事件(CPU 密集)
  buttons.forEach(button => {
    // 3. 解析指令、建立响应式连接(CPU 密集)
    button.addEventListener('click', () => {
      // 更新数据、重新渲染...
    })
    
    // 4. 恢复组件状态(CPU 密集)
    const initialValue = button.textContent
    // 建立 Vue 的响应式系统...
  })
}


什么时候不需要水合

  1. 小型应用:组件少于 5 个,水合成本低
  2. 首屏简单:没有复杂交互组件


assistant