数据缓存与请求去重策略
应用的性能瓶颈往往不在渲染,而在数据获取——一次网络请求动辄几十到几百毫秒,同一数据被多个组件反复请求更是常见浪费。Nuxt4 内置了从 Payload 提取、请求去重到 SWR 模式的完整缓存体系。本章深入每一层缓存机制,并用 AI 视频项目的多级缓存设计作为综合实战。
1. getCachedData 自定义缓存
1.1 默认缓存行为
useFetch 的默认缓存逻辑:
首次 SSR 请求:
服务端获取数据 → 存入 Payload → 客户端从 Payload 读取(不再请求)
客户端导航到新页面:
检查 Payload 中是否有缓存 → 没有 → 发起请求
客户端导航回已访问的页面:
默认会重新获取(不缓存跨页面数据)
1.2 getCachedData 函数签名
const { data } = await useFetch('/api/videos', {
getCachedData(key: string, nuxtApp: NuxtApp): T | undefined {
// 返回值决定是否使用缓存:
// - 返回 undefined/null → 发起新请求
// - 返回数据 → 使用缓存,跳过请求
},
})1.3 常用缓存模式
模式 1:会话级缓存(永不过期)
// 整个用户会话期间只请求一次
const { data: categories } = await useFetch('/api/categories', {
getCachedData(key, nuxtApp) {
return nuxtApp.payload.data[key] || nuxtApp.static.data[key]
},
})
// 适用于:分类列表、配置数据等不频繁变化的数据模式 2:TTL 缓存(指定时间后过期)
// app/composables/useCachedFetch.ts
export function useCachedFetch<T>(url: string, ttlSeconds: number, options: any = {}) {
return useFetch<T>(url, {
...options,
getCachedData(key, nuxtApp) {
const cached = nuxtApp.payload.data[key]
if (!cached) return undefined
// 检查缓存时间戳
const fetchedAt = nuxtApp.payload._fetchedAt?.[key]
if (!fetchedAt) return cached
const age = (Date.now() - fetchedAt) / 1000
if (age > ttlSeconds) return undefined // 过期
return cached // 未过期,使用缓存
},
// 请求成功后记录时间戳
onResponse() {
const nuxtApp = useNuxtApp()
nuxtApp.payload._fetchedAt = nuxtApp.payload._fetchedAt || {}
nuxtApp.payload._fetchedAt[`$f${url}`] = Date.now()
},
})
}使用:
<script setup lang="ts">
// 视频列表缓存 2 分钟
const { data: videos } = await useCachedFetch('/api/videos', 120, {
query: { page: 1 },
})
// 分类数据缓存 10 分钟
const { data: categories } = await useCachedFetch('/api/categories', 600)
</script>模式 3:条件缓存
const { data } = await useFetch('/api/user/profile', {
getCachedData(key, nuxtApp) {
const cached = nuxtApp.payload.data[key]
if (!cached) return undefined
// 只在客户端导航时使用缓存,SSR 时总是获取最新数据
if (import.meta.server) return undefined
return cached
},
})2. Payload 提取机制
2.1 Payload 的生命周期
① SSR 阶段:
useFetch('/api/videos')
→ 服务端获取数据
→ 数据存入 nuxtApp.payload.data['$f/api/videos']
→ 序列化到 HTML:<script>window.__NUXT__={...}</script>
② Hydration 阶段:
客户端 Vue 启动
→ 从 window.__NUXT__ 恢复 payload
→ useFetch 发现 payload 中有缓存 → 直接使用,不请求
③ 客户端导航:
导航到新页面
→ useFetch 检查 payload(通常没有)
→ 发起 HTTP 请求
→ 数据存入 nuxtApp.payload.data(内存中)
2.2 payload 与 static 数据的区别
getCachedData(key, nuxtApp) {
// payload.data:SSR 和客户端导航获取的数据
const dynamic = nuxtApp.payload.data[key]
// static.data:预渲染(SSG)时获取的数据
const prerendered = nuxtApp.static.data[key]
return dynamic || prerendered
}payload.data:SSR 和 CSR 时获取的数据,存在内存中static.data:nuxi generate预渲染时获取的数据,打包到静态 JSON 文件中
2.3 手动操作 Payload
const nuxtApp = useNuxtApp()
// 手动写入缓存
nuxtApp.payload.data['custom-key'] = { foo: 'bar' }
// 手动读取缓存
const cached = nuxtApp.payload.data['custom-key']
// 手动清除缓存
delete nuxtApp.payload.data['custom-key']
// 清除所有缓存(强制所有 useFetch 重新获取)
nuxtApp.payload.data = {}3. dedupe 避免重复请求
3.1 什么是请求去重
当同一个 useFetch 被多次调用(如多个组件引用同一数据、快速连续导航)时,dedupe 控制如何处理并发请求:
const { data } = await useFetch('/api/videos', {
dedupe: 'cancel', // 默认值
})3.2 dedupe 选项
| 值 | 行为 | 适用场景 |
|---|---|---|
'cancel'(默认) | 新请求取消旧的未完成请求 | 搜索输入、筛选切换 |
'defer' | 新请求等待旧请求完成,复用其结果 | 多组件共享同一数据 |
3.3 cancel 模式示例
const query = ref('vue')
const { data: results } = await useFetch('/api/search', {
query: { q: query },
dedupe: 'cancel',
// 用户快速输入:v → vu → vue
// cancel 模式:取消 'v' 和 'vu' 的请求,只保留 'vue'
})3.4 defer 模式示例
// 多个组件都需要用户信息
// 组件 A
const { data: user } = await useFetch('/api/auth/me', {
dedupe: 'defer',
})
// 组件 B(同时挂载)
const { data: user } = await useFetch('/api/auth/me', {
dedupe: 'defer',
})
// defer 模式:只发起一次请求,两个组件共享结果3.5 与 refresh 的配合
const { data, refresh } = await useFetch('/api/videos')
// 快速连续调用 refresh
refresh() // 请求 1
refresh() // dedupe: 'cancel' → 取消请求 1,发起请求 2
refresh() // 取消请求 2,发起请求 3
// 最终只有请求 3 完成4. SWR 模式
4.1 什么是 SWR
SWR(Stale-While-Revalidate):先返回缓存数据(stale),同时在后台获取最新数据(revalidate)。
用户导航到页面
↓
getCachedData 返回缓存数据 → 页面立即显示(0ms 延迟)
↓ 同时
后台发起请求获取最新数据 → 数据更新 → 页面自动刷新
4.2 客户端 SWR 实现
// app/composables/useSwrFetch.ts
export function useSwrFetch<T>(url: string, options: any = {}) {
const nuxtApp = useNuxtApp()
return useFetch<T>(url, {
...options,
// 总是返回缓存(如果有的话)
getCachedData(key) {
return nuxtApp.payload.data[key] || nuxtApp.static.data[key]
},
// 即使有缓存也后台刷新
dedupe: 'defer',
})
}4.3 使用场景
<script setup lang="ts">
// 视频列表:先显示缓存,后台静默刷新
const { data: videos, status } = useSwrFetch('/api/videos/trending')
// status === 'success' 且 data 有值 → 显示缓存数据
// 后台请求完成后 → data 自动更新为最新数据
</script>
<template>
<div>
<!-- 即使在刷新中,也始终有数据显示 -->
<VideoGrid :videos="videos" />
<!-- 可选:显示刷新指示器 -->
<span v-if="status === 'pending'" class="text-xs text-gray-400">
更新中...
</span>
</div>
</template>4.4 服务端 SWR(routeRules)
上一章讲过的 swr routeRule 是服务端级别的 SWR,作用于 Nitro 层面:
// nuxt.config.ts
routeRules: {
'/api/videos/trending': { swr: 60 },
// Nitro 缓存 API 响应 60 秒
// 过期后返回旧响应 + 后台刷新
}客户端 SWR(getCachedData)和服务端 SWR(routeRules)可以叠加使用。
5. AI 视频列表多级缓存设计
5.1 缓存层级架构
请求:GET /api/videos/trending
L1: 客户端内存缓存(getCachedData)
↓ 未命中
L2: Payload 缓存(SSR → CSR 传递的数据)
↓ 未命中
L3: HTTP 缓存(浏览器 Cache / CDN)
↓ 未命中
L4: Nitro 服务端缓存(routeRules swr/isr)
↓ 未命中
L5: 源站 API(实际数据库查询)
5.2 完整实现
// nuxt.config.ts — L3 + L4 层
export default defineNuxtConfig({
routeRules: {
// API 级别缓存(L4)
'/api/videos/trending': {
swr: 60,
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
},
},
'/api/categories': {
swr: 600,
headers: {
'Cache-Control': 'public, s-maxage=600, stale-while-revalidate=3600',
},
},
},
})// app/composables/useCachedFetch.ts — L1 + L2 层
type CachePolicy = 'no-cache' | 'session' | number
export function useCachedFetch<T>(
url: string | (() => string),
options: { cache?: CachePolicy; [key: string]: any } = {}
) {
const { cache = 'no-cache', ...fetchOptions } = options
const nuxtApp = useNuxtApp()
const getCachedData = cache === 'no-cache'
? undefined
: (key: string) => {
const cached = nuxtApp.payload.data[key] || nuxtApp.static.data[key]
if (!cached) return undefined
if (cache === 'session') return cached // 永不过期
// TTL 检查
const fetchedAt = nuxtApp.payload._fetchedAt?.[key]
if (fetchedAt && (Date.now() - fetchedAt) / 1000 > cache) {
return undefined // 过期
}
return cached
}
return useFetch<T>(url, {
...fetchOptions,
getCachedData,
})
}5.3 在页面中使用
<!-- app/pages/index.vue — 首页 -->
<script setup lang="ts">
// 热门视频:客户端缓存 2 分钟 + 服务端 SWR 1 分钟
const { data: trending } = await useCachedFetch('/api/videos/trending', {
cache: 120,
pick: ['id', 'title', 'thumbnail', 'views'],
})
// 分类列表:整个会话缓存(很少变化)
const { data: categories } = await useCachedFetch('/api/categories', {
cache: 'session',
})
// 用户推荐:不缓存(个性化数据)
const { data: recommended } = await useFetch('/api/videos/recommended', {
headers: useRequestHeaders(['cookie']),
})
</script>5.4 缓存失效策略
// 手动清除特定缓存(如用户操作后需要刷新数据)
function invalidateCache(key: string) {
const nuxtApp = useNuxtApp()
delete nuxtApp.payload.data[key]
delete nuxtApp.payload._fetchedAt?.[key]
}
// 使用场景:上传视频后清除列表缓存
async function handleUpload(videoData: FormData) {
await $fetch('/api/videos', { method: 'POST', body: videoData })
// 清除相关缓存
invalidateCache('$f/api/videos/trending')
invalidateCache('$f/api/videos')
// 刷新当前页面的数据
refreshNuxtData() // Nuxt 全局方法,刷新所有 useAsyncData
}// 选择性刷新
await refreshNuxtData('videos-trending') // 只刷新指定 key
await refreshNuxtData(['videos', 'stats']) // 刷新多个 key5.5 缓存策略决策表
| 数据类型 | L1 客户端 | L4 服务端 | 理由 |
|---|---|---|---|
| 分类/标签列表 | session | swr: 600 | 几乎不变 |
| 热门视频列表 | 120s | swr: 60 | 分钟级变化 |
| 视频详情 | 60s | isr: 60 | 偶尔更新(标题/描述) |
| 评论列表 | no-cache | swr: 10 | 频繁变化 |
| 用户个人数据 | no-cache | 无 | 个性化,不可缓存 |
| 搜索结果 | no-cache | swr: 30 | 查询参数多变 |
本章小结
- getCachedData:控制
useFetch何时使用缓存、何时重新获取;支持会话缓存、TTL、条件缓存 - Payload 提取:SSR 数据通过 Payload 传递到客户端,避免 Hydration 后重复请求
- dedupe:
cancel(默认)取消旧请求、defer复用并发请求结果 - SWR 模式:先显示缓存 → 后台静默刷新 → 自动更新界面
- 多级缓存:L1 客户端内存 → L2 Payload → L3 HTTP/CDN → L4 Nitro 服务端 → L5 源站
- 缓存失效:
invalidateCache手动清除 +refreshNuxtData刷新指定数据
至此,第三篇"渲染与数据获取"全部 6 章已完成。你已经掌握了:
- SSR 水合机制与生命周期差异
- SSG 预渲染与 ISR 增量再生
- 混合渲染架构设计与 CDN 缓存策略
- useFetch 与 useAsyncData 的深度差异
- $fetch 底层封装与 BFF 接口层安全实践
- 多级数据缓存与请求去重策略
下一篇将深入 组件系统精讲。