在 Nuxt 3 项目中,useFetch 是最常用的数据获取组合式函数。不少团队在落地服务端渲染时发现,即便接口响应很快,首屏依然要等上几百毫秒才能看到内容,或者页面先闪一下空态再突然渲染出数据。这种现象背后通常和 SSR 的数据预取机制、客户端水合逻辑以及全局请求拦截器的注册时机有关。理解清楚这三者的协作方式,才能从根本上消除不必要的延迟。

一、useFetch 在 SSR 中的默认行为
Nuxt 的 useFetch 在组件 setup 阶段执行时,如果当前处于服务端渲染上下文,它会立即发起对应的 HTTP 请求,并将返回结果通过 payload 的形式序列化进最终生成的 HTML 页面中。浏览器拿到页面后,Vue 应用进行 hydration,此时 useFetch 会优先从内联的 payload 里恢复数据,而不是重新请求一次接口。这种设计本应让首屏直接带着数据呈现。
但在实际工程中,很多开发者会在客户端插件里统一配置 axios 或 ofetch 的拦截器,用来添加 token、处理错误。如果拦截器只在客户端注册,那么服务端执行 useFetch 时就不会走这些逻辑,导致服务端请求因为没有鉴权头而失败,Nuxt 只能降级到客户端再去请求,于是出现了肉眼可见的延迟与闪烁。下面是一段典型的错误用法:
// plugins/axios.client.ts 只在客户端运行的插件
export default defineNuxtPlugin((nuxtApp) => {
const api = axios.create()
api.interceptors.request.use((config) => {
config.headers.token = localStorage.getItem('token')
return config
})
nuxtApp.provide('api', api)
})
上述代码使用 .client 后缀,意味着它不会在服务端执行。当页面需要 SSR 时,useFetch 内部若依赖了这个 api 实例,服务端根本拿不到 token,请求被后端拒绝,只能等客户端接管后重新获取。这就是延迟的首要来源。
二、拦截器注册时机导致的重复请求
要避免服务端拿不到拦截逻辑,应当把基础请求实例和拦截器注册到通用的 Nuxt 插件中,而不是带 .client 后缀的文件。同时,如果 token 来自 cookie 而非 localStorage,服务端也能正常读取。这样无论服务端还是客户端,useFetch 都会使用同一套带拦截器的实例,保证两端行为一致。
下面给出一个同时覆盖两端的插件示例,在请求拦截里优先从 cookie 取凭证,客户端再兜底到本地存储:
// plugins/api.ts 通用插件,服务端客户端均执行
import axios from 'axios'
export default defineNuxtPlugin((nuxtApp) => {
const api = axios.create({
baseURL: 'https://api.ipipp.com'
})
api.interceptors.request.use((config) => {
// 服务端从 cookie 读取,客户端从 localStorage 读取
const token = process.server
? useCookie('token').value
: localStorage.getItem('token')
if (token) {
config.headers.Authorization = 'Bearer ' + token
}
return config
})
api.interceptors.response.use(
(res) => res,
(err) => {
// 统一错误上报
console.error('request failed', err.message)
return Promise.reject(err)
}
)
nuxtApp.provide('api', api)
})
通过这种方式,服务端渲染时 useFetch 调用的就是带拦截器的实例,请求能正常携带凭证并返回数据,HTML 里直接内联了结果。客户端 hydration 时不会再发重复请求,首屏延迟自然消失。需要注意,使用 useCookie 而非直接读 document.cookie,才能兼容 SSR 上下文。
三、利用 useFetch 参数进一步压低延迟
除了拦截器问题,useFetch 自身的一些参数也会影响感知性能。默认情况下 useFetch 是阻塞导航的,也就是说在请求回来之前,页面不会完成切换。对于非关键数据,可以设置 lazy: true,让页面先渲染骨架,数据回来再填充,从而提升交互响应速度。
另外,如果某部分数据只需要在客户端获取(比如依赖浏览器地理定位),可以传 server: false,避免服务端做无用请求。反之,核心首屏数据应保持 server 默认为 true,并确保拦截器两端一致。下面的表格对比了不同组合的效果:
| 参数组合 | 服务端是否请求 | 客户端是否重复请求 | 适用场景 |
|---|---|---|---|
| 默认(server true, lazy false) | 是 | 否(payload 恢复) | 首屏核心数据 |
| lazy true | 是 | 否 | 允许骨架屏的非阻塞数据 |
| server false | 否 | 是(仅客户端) | 纯浏览器端能力数据 |
从表里可以看出,只要服务端请求成功且拦截器无误,客户端就不会重复请求。开发时应当用浏览器开发者工具的 Network 面板确认 HTML 文档里是否已包含 __NUXT__ 形式的 payload,以此验证 SSR 数据预取是否生效。
四、常见误区与排查清单
一个被广泛忽略的误区是:把全部鉴权与错误处理都写在页面组件的 onMounted 里,然后手动调 axios。这种做法完全绕开了 useFetch 的 SSR 能力,导致所有数据只能在客户端加载,延迟最大化。正确思路是尽量使用 Nuxt 提供的组合式函数,并把副作用收敛到插件拦截器中。
排查延迟问题时,建议按以下顺序检查:确认插件是否带 .client 后缀;确认 token 来源在服务端是否可读;在页面源码中搜索序列化数据是否存在;用 Network 看客户端是否发了同名接口。只要拦截器两端统一且 useFetch 参数合理,Nuxt 的数据访问延迟就能控制在网络往返时间之内,不再有额外的水合等待。
小结:Nuxt useFetch 的延迟大多不是框架缺陷,而是拦截器注册位置与 SSR 上下文读取凭证的方式不对。把请求实例做成通用插件,区分服务端和客户端的 token 获取路径,再配合 lazy 与 server 参数,就能彻底解决重复请求与首屏等待问题。