缓存策略体系

缓存是性能优化中投入产出比最高的手段——一次命中缓存,直接跳过网络传输、服务端计算、数据库查询。但缓存也是最容易出问题的——过期数据、缓存穿透、缓存雪崩,每个都能让系统崩溃。本章系统梳理 Nuxt4 从浏览器到 CDN 到服务端的多级缓存体系,建立清晰的缓存心智模型。

1. 缓存分层架构

1.1 四级缓存模型

一个 Nuxt4 全栈应用可以建立四级缓存:

用户请求 → ① 浏览器缓存 → ② CDN 边缘缓存 → ③ Nitro 服务端缓存 → ④ 数据库/外部 API
         (最快,0ms)   (快,10-50ms)    (中,50-200ms)      (慢,200-2000ms)

每一级缓存都能拦截请求,避免打到下一级。越靠近用户的缓存越快,但更新越难——这是缓存设计的核心权衡。

1.2 缓存策略矩阵

资源类型变化频率推荐缓存层Cache-Control
静态资源(JS/CSS/图片)极低(带 hash)浏览器 + CDNmax-age=31536000, immutable
首页 HTML中(每分钟变)CDN + Nitro SWRs-maxage=60, stale-while-revalidate=600
视频列表 APINitro 缓存max-age=30
视频详情 APINitro 缓存 + CDNs-maxage=3600
用户个人数据高(实时)不缓存no-store
AI 生成状态极高(秒级变化)不缓存no-cache

2. HTTP 缓存头

2.1 Cache-Control 核心指令

指令含义适用场景
max-age=N浏览器缓存 N 秒静态资源、不常变的 API
s-maxage=NCDN/代理缓存 N 秒(覆盖 max-age)CDN 缓存
no-cache必须向服务端验证(ETag/Last-Modified)需要实时性但可以用 304
no-store完全不缓存敏感数据(用户信息、Token)
stale-while-revalidate=N缓存过期后仍返回旧数据,同时后台刷新SWR 模式
immutable内容永远不变(配合文件名 hash)带 hash 的 JS/CSS
private仅浏览器缓存,CDN 不缓存用户专属数据
public允许 CDN 缓存公开资源

2.2 ETag 与条件请求

ETag 是资源的"指纹"——服务端返回 ETag: "abc123",浏览器再次请求时携带 If-None-Match: "abc123"。如果资源没变,服务端返回 304 Not Modified(无 body),浏览器使用本地缓存。

浏览器 → GET /api/videos/123 
服务端 ← 200 OK, ETag: "v1", Cache-Control: no-cache
       (首次请求,返回完整数据)

浏览器 → GET /api/videos/123, If-None-Match: "v1"
服务端 ← 304 Not Modified
       (数据没变,不传 body,节省带宽)

2.3 Vary 头

Vary 告诉 CDN 按哪些请求头区分缓存。常见场景:

Vary: Accept-Encoding    → gzip 和 br 版本分开缓存
Vary: Accept-Language     → 不同语言的页面分开缓存
Vary: Cookie              → ⚠️ 危险:每个用户一份缓存,等于不缓存

黄金法则:永远不要在公开资源上设置 Vary: Cookie,否则 CDN 缓存完全失效。

3. Nitro 路由级缓存(routeRules)

3.1 基本配置

Nuxt4 / Nitro 的 routeRules 可以为不同路由设置不同的缓存策略:

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // 静态页面:CDN 缓存 1 天,浏览器缓存 1 小时
    '/about': { headers: { 'Cache-Control': 's-maxage=86400, max-age=3600' } },
 
    // 视频列表:ISR 模式,CDN 缓存 60 秒,过期后 SWR
    '/videos': { isr: 60 },
 
    // 视频详情:SSR + CDN 缓存 1 小时
    '/videos/**': { isr: 3600 },
 
    // API:服务端缓存 30 秒
    '/api/videos': { cache: { maxAge: 30 } },
 
    // 用户 API:不缓存
    '/api/user/**': { cache: false, headers: { 'Cache-Control': 'no-store' } },
  },
})

3.2 ISR 模式深度解析

isr: NIncremental Static Regeneration 的实现——首次请求 SSR 渲染,结果缓存 N 秒。后续请求直接返回缓存的 HTML,无需 SSR。缓存过期后,下一次请求触发后台重新渲染,同时返回旧缓存(SWR 模式)。

请求 1(缓存空)→ SSR 渲染 → 缓存 HTML → 返回(200ms)
请求 2(缓存命中)→ 直接返回缓存 → (5ms)
请求 3(缓存过期)→ 返回旧缓存 + 后台重新渲染 → (5ms,后台刷新)
请求 4(新缓存)→ 返回新渲染结果 → (5ms)

这对于内容不频繁变化的页面(视频详情、文章页)是最优策略——首次快、复访极快、数据最终一致

3.3 defineCachedEventHandler

在 API 层面,可以用 defineCachedEventHandler 缓存 API 响应:

// server/api/videos/popular.get.ts
export default defineCachedEventHandler(async (event) => {
  const videos = await db.select().from(videosTable)
    .orderBy(desc(videosTable.views))
    .limit(20)
  return videos
}, {
  maxAge: 60,       // 缓存 60 秒
  staleMaxAge: 300, // 过期后仍可返回旧数据 300 秒(SWR)
  name: 'popular-videos',
  getKey: (event) => 'popular',  // 缓存 key
})

3.4 缓存存储后端

Nitro 默认用内存缓存,生产环境可以切换到 Redis:

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    storage: {
      cache: {
        driver: 'redis',
        host: process.env.REDIS_HOST,
        port: 6379,
        db: 0,
      },
    },
  },
})

4. 客户端数据缓存

4.1 useFetch / useAsyncData 的内置缓存

Nuxt 的数据获取 Composable 自带 Payload 缓存机制:

  • SSR → CSR:服务端获取的数据通过 Payload 传递,客户端 Hydration 时不会重新请求
  • 客户端导航:默认不缓存(每次导航重新请求),除非手动配置 getCachedData

4.2 getCachedData 自定义缓存

const { data } = await useFetch('/api/videos/popular', {
  key: 'popular-videos',
  getCachedData(key, nuxtApp) {
    // 如果缓存中有数据且不超过 30 秒,直接返回
    const cached = nuxtApp.payload.data[key] || nuxtApp.static.data[key]
    if (cached) return cached
    return null  // null 表示需要重新请求
  },
})

4.3 SWR 模式客户端实现

const { data, refresh } = await useFetch('/api/videos', {
  key: 'video-list',
  getCachedData(key, nuxtApp) {
    return nuxtApp.payload.data[key]  // 先返回旧数据
  },
})
 
// 页面可见时后台刷新
onMounted(() => {
  document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'visible') {
      refresh()  // 后台刷新,不阻塞 UI
    }
  })
})

5. Redis 服务端缓存

5.1 适用场景

Redis 缓存适合:

  • 热点数据:首页推荐、热门视频排行(读多写少)
  • 计算密集结果:AI 生成结果、聚合统计
  • 会话数据:用户在线状态、限流计数器

5.2 缓存模式

Cache-Aside(旁路缓存):最常用的模式

// server/utils/cache.ts
export async function cachedQuery<T>(
  key: string,
  queryFn: () => Promise<T>,
  ttl = 60
): Promise<T> {
  const storage = useStorage('cache')
 
  // 1. 先查缓存
  const cached = await storage.getItem<T>(key)
  if (cached) return cached
 
  // 2. 缓存未命中,查数据库
  const result = await queryFn()
 
  // 3. 写入缓存
  await storage.setItem(key, result, { ttl })
 
  return result
}
// server/api/videos/popular.get.ts
export default defineEventHandler(async () => {
  return cachedQuery('videos:popular', async () => {
    return db.select().from(videosTable)
      .orderBy(desc(videosTable.views))
      .limit(20)
  }, 60)
})

5.3 缓存失效策略

策略实现适用场景
TTL 过期设置 ttl 自动过期热门列表、排行榜
主动失效数据更新时 storage.removeItem(key)视频详情(编辑后立即刷新)
版本号key 中包含版本号 v2:videos:popular数据结构变更
标签失效按标签批量删除 invalidateTag('videos')批量更新场景

5.4 缓存穿透与雪崩防护

缓存穿透:请求不存在的数据,每次都打到数据库。

解决:缓存空结果(null),设置短 TTL:

const result = await queryFn()
// 即使结果为空,也缓存(避免穿透)
await storage.setItem(key, result ?? '__NULL__', { ttl: result ? 60 : 10 })

缓存雪崩:大量缓存同时过期,瞬间压力打到数据库。

解决:TTL 加随机偏移量:

const jitter = Math.floor(Math.random() * 10)  // 0-10 秒随机偏移
await storage.setItem(key, result, { ttl: ttl + jitter })

6. Service Worker 离线缓存

6.1 @vite-pwa/nuxt 集成

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@vite-pwa/nuxt'],
  pwa: {
    strategies: 'generateSW',
    registerType: 'autoUpdate',
    workbox: {
      navigateFallback: '/',
      globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
      runtimeCaching: [
        {
          urlPattern: /^https:\/\/api\.example\.com\/videos/,
          handler: 'StaleWhileRevalidate',
          options: { cacheName: 'api-videos', expiration: { maxEntries: 50 } },
        },
        {
          urlPattern: /\.(jpg|jpeg|png|webp|avif)$/,
          handler: 'CacheFirst',
          options: { cacheName: 'images', expiration: { maxEntries: 200, maxAgeSeconds: 86400 * 30 } },
        },
      ],
    },
  },
})

6.2 缓存策略选择

策略行为适用资源
CacheFirst先查缓存,未命中才网络请求图片、字体、不变的 API
NetworkFirst先网络请求,失败用缓存兜底HTML 页面、实时数据
StaleWhileRevalidate返回缓存 + 后台更新API 数据(可接受短暂过期)
NetworkOnly只用网络实时数据(AI 生成状态)
CacheOnly只用缓存离线专用资源

7. 缓存调试与排查

7.1 浏览器 DevTools

在 Network 面板中查看:

  • Size 列:显示 (disk cache)(memory cache) 表示命中浏览器缓存
  • Status 列304 表示条件请求命中(ETag/Last-Modified)
  • Response Headers:检查 Cache-ControlETagAge

7.2 CDN 缓存验证

# 检查 CDN 缓存状态
curl -I https://example.com/api/videos/popular
# 看 X-Cache: HIT / MISS
# 看 Age: 45(已缓存 45 秒)
# 看 Cache-Control: s-maxage=60

7.3 常见问题

问题原因解决
CDN 始终 MISS设置了 Vary: Cookie移除 Vary: Cookie,用 private
页面显示旧数据缓存 TTL 过长缩短 TTL 或主动失效
API 返回旧数据Nitro 缓存未清除重启服务或清除 Redis
登录后仍显示未登录HTML 被 CDN 缓存用户相关页面设置 private

本章小结

  • 四级缓存:浏览器 → CDN → Nitro 服务端 → 数据库,越靠近用户越快
  • Cache-Controlimmutable 静态资源、s-maxage + SWR 公开页面、no-store 敏感数据
  • ISRrouteRulesisr: N 实现增量静态再生,首次 SSR 后缓存 HTML
  • API 缓存defineCachedEventHandler 服务端缓存 API 响应
  • Redis:Cache-Aside 模式,TTL + 主动失效,防穿透(缓存空值)防雪崩(TTL 抖动)
  • 客户端缓存getCachedData 自定义缓存策略,visibilitychange 实现 SWR
  • Service Worker:CacheFirst 静态资源,StaleWhileRevalidate API 数据