客户端 vs 服务端获取与 hydration
本教程共 50 篇 · 第 24 篇 · 更新于 2026-08-08 · 约 8 分钟阅读
本节目标:理解同一段取数代码在服务器和浏览器两端如何运行,弄清「水合不匹配」的成因并学会规避。
24-1
Nuxt 最迷人的地方,是「同构」:你写的一个组件,既会在服务器上跑一遍(生成 HTML),又会在浏览器里跑一遍(让页面变交互)。拿数据这件事,在这两端的表现并不一样。
当请求到来时,Nuxt 在服务器上:
- 创建 Vue 和 Nuxt 实例,执行插件;
- 跑路由校验、中间件;
- 渲染页面及其组件,期间执行
useFetch/useAsyncData去取数; - 把渲染好的 HTML 连同 payload(取到的数据)一起发给浏览器。
Note服务器上没有真正的「响应式更新」——Vue 是把整棵组件树从上到下渲染成静态 HTML。所以
onMounted、onBeforeMount这些浏览器生命周期钩子在 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
记住三件事,基本能避开九成的水合问题:
- 取数据一律用
useFetch/useAsyncData/useState这些「SSR 友好」的组合式函数,别裸写$fetch。 - 浏览器专属代码放进
onMounted,或用<ClientOnly>包裹。 - 服务器和浏览器要用同一份数据源,不要一边随机、一边固定。
Note强制要求客户端才取数时,可给
useFetch加server: 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.innerWidth、localStorage 或 Date.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 过程,显著减少客户端的工作量。这个特性对于内容型网站(如博客、文档站)的性能提升尤为明显。