数据缓存与请求去重策略

应用的性能瓶颈往往不在渲染,而在数据获取——一次网络请求动辄几十到几百毫秒,同一数据被多个组件反复请求更是常见浪费。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.datanuxi 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']) // 刷新多个 key

5.5 缓存策略决策表

数据类型L1 客户端L4 服务端理由
分类/标签列表sessionswr: 600几乎不变
热门视频列表120sswr: 60分钟级变化
视频详情60sisr: 60偶尔更新(标题/描述)
评论列表no-cacheswr: 10频繁变化
用户个人数据no-cache个性化,不可缓存
搜索结果no-cacheswr: 30查询参数多变

本章小结

  • getCachedData:控制 useFetch 何时使用缓存、何时重新获取;支持会话缓存、TTL、条件缓存
  • Payload 提取:SSR 数据通过 Payload 传递到客户端,避免 Hydration 后重复请求
  • dedupecancel(默认)取消旧请求、defer 复用并发请求结果
  • SWR 模式:先显示缓存 → 后台静默刷新 → 自动更新界面
  • 多级缓存:L1 客户端内存 → L2 Payload → L3 HTTP/CDN → L4 Nitro 服务端 → L5 源站
  • 缓存失效invalidateCache 手动清除 + refreshNuxtData 刷新指定数据

至此,第三篇"渲染与数据获取"全部 6 章已完成。你已经掌握了:

  1. SSR 水合机制与生命周期差异
  2. SSG 预渲染与 ISR 增量再生
  3. 混合渲染架构设计与 CDN 缓存策略
  4. useFetch 与 useAsyncData 的深度差异
  5. $fetch 底层封装与 BFF 接口层安全实践
  6. 多级数据缓存与请求去重策略

下一篇将深入 组件系统精讲