缓存策略体系
缓存是性能优化中投入产出比最高的手段——一次命中缓存,直接跳过网络传输、服务端计算、数据库查询。但缓存也是最容易出问题的——过期数据、缓存穿透、缓存雪崩,每个都能让系统崩溃。本章系统梳理 Nuxt4 从浏览器到 CDN 到服务端的多级缓存体系,建立清晰的缓存心智模型。
1. 缓存分层架构
1.1 四级缓存模型
一个 Nuxt4 全栈应用可以建立四级缓存:
用户请求 → ① 浏览器缓存 → ② CDN 边缘缓存 → ③ Nitro 服务端缓存 → ④ 数据库/外部 API
(最快,0ms) (快,10-50ms) (中,50-200ms) (慢,200-2000ms)
每一级缓存都能拦截请求,避免打到下一级。越靠近用户的缓存越快,但更新越难——这是缓存设计的核心权衡。
1.2 缓存策略矩阵
| 资源类型 | 变化频率 | 推荐缓存层 | Cache-Control |
|---|---|---|---|
| 静态资源(JS/CSS/图片) | 极低(带 hash) | 浏览器 + CDN | max-age=31536000, immutable |
| 首页 HTML | 中(每分钟变) | CDN + Nitro SWR | s-maxage=60, stale-while-revalidate=600 |
| 视频列表 API | 中 | Nitro 缓存 | max-age=30 |
| 视频详情 API | 低 | Nitro 缓存 + CDN | s-maxage=3600 |
| 用户个人数据 | 高(实时) | 不缓存 | no-store |
| AI 生成状态 | 极高(秒级变化) | 不缓存 | no-cache |
2. HTTP 缓存头
2.1 Cache-Control 核心指令
| 指令 | 含义 | 适用场景 |
|---|---|---|
max-age=N | 浏览器缓存 N 秒 | 静态资源、不常变的 API |
s-maxage=N | CDN/代理缓存 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: N 是 Incremental 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-Control、ETag、Age头
7.2 CDN 缓存验证
# 检查 CDN 缓存状态
curl -I https://example.com/api/videos/popular
# 看 X-Cache: HIT / MISS
# 看 Age: 45(已缓存 45 秒)
# 看 Cache-Control: s-maxage=607.3 常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
| CDN 始终 MISS | 设置了 Vary: Cookie | 移除 Vary: Cookie,用 private |
| 页面显示旧数据 | 缓存 TTL 过长 | 缩短 TTL 或主动失效 |
| API 返回旧数据 | Nitro 缓存未清除 | 重启服务或清除 Redis |
| 登录后仍显示未登录 | HTML 被 CDN 缓存 | 用户相关页面设置 private |
本章小结
- 四级缓存:浏览器 → CDN → Nitro 服务端 → 数据库,越靠近用户越快
- Cache-Control:
immutable静态资源、s-maxage + SWR公开页面、no-store敏感数据 - ISR:
routeRules的isr: N实现增量静态再生,首次 SSR 后缓存 HTML - API 缓存:
defineCachedEventHandler服务端缓存 API 响应 - Redis:Cache-Aside 模式,TTL + 主动失效,防穿透(缓存空值)防雪崩(TTL 抖动)
- 客户端缓存:
getCachedData自定义缓存策略,visibilitychange实现 SWR - Service Worker:CacheFirst 静态资源,StaleWhileRevalidate API 数据