首页 / Nuxt 4 入门教程 / 客户端 vs 服务端获取与 hydration

Nuxt 4 入门教程

客户端 vs 服务端获取与 hydration

本教程共 50 篇 · 第 24 篇 · 更新于 2026-08-08 · 约 8 分钟阅读

NuxtNuxt4hydration数据获取SSR

本节目标:理解同一段取数代码在服务器和浏览器两端如何运行,弄清「水合不匹配」的成因并学会规避。

24-1

Nuxt 最迷人的地方,是「同构」:你写的一个组件,既会在服务器上跑一遍(生成 HTML),又会在浏览器里跑一遍(让页面变交互)。拿数据这件事,在这两端的表现并不一样。

当请求到来时,Nuxt 在服务器上:

  1. 创建 Vue 和 Nuxt 实例,执行插件;
  2. 跑路由校验、中间件;
  3. 渲染页面及其组件,期间执行 useFetch / useAsyncData 去取数;
  4. 把渲染好的 HTML 连同 payload(取到的数据)一起发给浏览器。
Note

服务器上没有真正的「响应式更新」——Vue 是把整棵组件树从上到下渲染成静态 HTML。所以 onMountedonBeforeMount 这些浏览器生命周期钩子在 SSR 阶段根本不会触发。这段认知很关键,后面会用到。

浏览器收到 HTML 后,再做「水合」(hydration):Vue 重新跑一遍组件,把服务器生成的 DOM 节点和自己的虚拟节点一一对应上,然后挂上事件监听,让页面「活」起来。

24-2

如果 setup 里直接写 $fetch,那服务器渲染时请求一次、浏览器水合时又请求一次。这还只是浪费。更糟的是:这两次请求可能不在同一个环境里,拿到的数据可能不同,于是水合时 Vue 比对发现「服务端画的」和「客户端算的」对不上,就报 hydration mismatch。

useFetch / useAsyncData 的巧妙之处正是:服务器取到的数据被塞进 payload,浏览器水合时直接复用,不再发第二次请求。两边拿到的数据天然一致,也就不会 mismatch。

<script setup lang="ts">
// ✅ SSR 安全:服务器取数,浏览器复用,不重复请求
const { data } = await useFetch('/api/news')
</script>

24-3

「水合不匹配」(hydration mismatch)指:服务器生成的 HTML 内容,和浏览器水合时算出来的内容不一样。Vue 在开发模式下会在控制台打印警告,提醒你注意。

它不只是个警告那么简单:

  • 性能变差:Vue 可能要重渲染整棵组件树,页面变「可交互」的时间被拉长。
  • 交互失灵:事件监听可能没正确挂上,按钮、表单点了没反应。
  • 状态混乱:用户看到的内容和框架认为渲染的内容对不上。
  • SEO 受损:搜索引擎可能抓到和真实页面不一致的内容。
Warning

开发时看到 hydration 警告,千万别无视。它是「应用可能出毛病」的信号,越早处理越好。

24-4

坑一:在服务器访问了浏览器才有的 API。 比如 localStorage 在服务器上根本不存在。

<script setup lang="ts">
// ❌ 服务器没有 localStorage,水合必不匹配
const userTheme = localStorage.getItem('theme') || 'light'
</script>

正确做法是用 useCookie 这类两端通用的 API:

<script setup lang="ts">
// ✅ 两端都能用
const userTheme = useCookie('theme', { default: () => 'light' })
</script>

坑二:用了随机值。 服务器和浏览器各自 Math.random(),结果一定不同。

<script setup lang="ts">
// ❌ 两边随机数不一样
const value = Math.random()
</script>

应该用 useState 让这个值在两端共享(下一章细讲):

<script setup lang="ts">
// ✅ 服务器生成一次,浏览器复用
const value = useState('rand', () => Math.random())
</script>

坑三:根据窗口宽度做条件渲染。 window.innerWidth 在服务器上取不到,也别在 setup 顶层这么写。优先用 CSS 媒体查询;必须 JS 判断时,放到 onMounted 里,或用 <ClientOnly> 包裹只在浏览器显示的部分。

坑四:依赖当前时间渲染。 new Date().getHours() 在服务器和浏览器可能跨秒、跨时区。需要的话用 <NuxtTime> 组件,或把这段逻辑放到 onMounted 之后。

Tip

一个通用原则:把「只在浏览器才安全」的代码,统统放进 onMounted,而不是写在 setup 顶层。比如初始化一个依赖 DOM 的第三方库,就等挂载完成再做。

24-5

在服务器的根作用域里,不要写「需要清理的副作用」。典型例子是用 setInterval 开了个定时器:在浏览器里你会用 onUnmounted 去清掉它,但服务器根本不会执行卸载钩子,定时器就永远留在那里,造成泄漏。

<script setup lang="ts">
// ❌ 服务器上 setInterval 不会被清理,会泄漏
const n = ref(0)
setInterval(() => n.value++, 1000)

// ✅ 改成浏览器挂载后再开
onMounted(() => {
  setInterval(() => n.value++, 1000)
})
</script>

24-6

记住三件事,基本能避开九成的水合问题:

  1. 取数据一律用 useFetch / useAsyncData / useState 这些「SSR 友好」的组合式函数,别裸写 $fetch
  2. 浏览器专属代码放进 onMounted,或用 <ClientOnly> 包裹。
  3. 服务器和浏览器要用同一份数据源,不要一边随机、一边固定。
Note

强制要求客户端才取数时,可给 useFetchserver: false,但要清楚:那样在服务器渲染阶段不会取数,必须自己处理加载态,且 data 在水合前是空的。

24-7

水合机制在 Nuxt 3 与 Nuxt 4 中一致,上述坑和规避方法通用。Nuxt 4 仅调整了文件目录(如 app/pages/),水合相关的组合式函数行为无变化。

24-8

理解「一次请求、两端执行」是写好 Nuxt 的关键。服务器负责先把数据和 HTML 备好,浏览器负责水合接管交互;useFetch 这类组合式函数通过 payload 让两端的取数结果保持一致,从而消除重复请求和水合不匹配。下一章我们深入取数之后的「刷新」与「缓存」机制。

24-7 Hydration 不匹配的排查

Hydration 不匹配是 SSR 项目中最常见的坑之一。它指的是服务端渲染的 HTML 和客户端 Vue 接管后期望的 DOM 结构不一致。出现这个问题时,浏览器控制台会报 “Hydration mismatch” 警告。

最常见的原因是在模板或组件 setup 中使用了只在浏览器端才存在的值,比如 window.innerWidthlocalStorageDate.now()。服务端没有这些 API,渲染出来的值和客户端不同,导致不匹配。

解决方法是把依赖浏览器环境的逻辑放到 onMounted 钩子里,或者用 import.meta.client 条件判断只在客户端执行。Nuxt 4 还提供了 <ClientOnly> 组件,可以包裹只在客户端渲染的内容,避免服务端和客户端的 DOM 差异。

24-8 优化 Hydration 的性能

Hydration 是 SSR 应用性能的关键环节。Vue 在客户端需要解析服务端返回的 HTML,绑定事件监听器,激活响应式系统。这个过程如果太慢,用户会感到页面”卡了一下”才能交互。

优化 hydration 的关键是减少客户端需要处理的 DOM 节点数量。避免在服务端渲染大量不必要的 HTML 元素。对于纯装饰性的内容(如动画效果),可以用 <ClientOnly> 包裹,让它们在 hydration 完成后再渲染。

Nuxt 4 对 hydration 做了进一步优化,支持选择性 hydration。这意味着页面上某些不交互的区域可以跳过 hydration 过程,显著减少客户端的工作量。这个特性对于内容型网站(如博客、文档站)的性能提升尤为明显。