缓存策略体系
Next.js 有四层缓存,从客户端到服务端层层叠加。理解这四层缓存是调优性能和解决"数据不更新"问题的关键。本章逐层讲清每种缓存的工作原理、默认行为、何时失效、以及如何手动控制。
1. 缓存全景
Next.js App Router 的四层缓存,从近到远:
浏览器 服务端
┌──────────────┐ ┌──────────────────────────────────────────┐
│ Router Cache │ → │ Full Route Cache → Data Cache → 数据源 │
│ (客户端) │ │ (静态 HTML) (fetch 结果) │
└──────────────┘ └──────────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ Request Memoization(单次请求内去重) │
└──────────────────────────────────────────┘
| 缓存层 | 位置 | 缓存什么 | 默认行为 | 失效方式 |
|---|---|---|---|---|
| Router Cache | 客户端(内存) | 访问过的路由的 RSC Payload | 自动缓存 30s(动态)/ 5min(静态) | router.refresh() / 导航 |
| Full Route Cache | 服务端(磁盘/CDN) | 静态渲染的完整 HTML + RSC Payload | 构建时生成,永久缓存 | revalidatePath / revalidateTag |
| Data Cache | 服务端(磁盘/CDN) | fetch() 的响应数据 | 永久缓存(需手动配置失效) | revalidate 时间 / revalidateTag |
| Request Memoization | 服务端(单次请求内存) | 同一次渲染中重复的 fetch() 调用 | 自动去重 | 请求结束后自动清除 |
2. Data Cache
2.1 原理
Data Cache 缓存 fetch() 请求的响应。在 Next.js 中,fetch 被扩展了——默认会把响应存入 Data Cache,下次相同请求直接返回缓存,不再请求数据源。
注意:Next.js 15 起,fetch 默认行为改为 no-store(不缓存)。需要显式启用缓存。这个改变是因为之前的默认缓存行为让很多开发者困惑——"为什么数据不更新?"
2.2 缓存策略
// 1. 不缓存 — 每次请求都查数据源(Next.js 15 默认行为)
const data = await fetch('https://api.example.com/data', {
cache: 'no-store',
})
// 2. 强制缓存 — 只在构建时请求一次,永久使用缓存
const data = await fetch('https://api.example.com/data', {
cache: 'force-cache',
})
// 3. 基于时间的重新验证(ISR 模式)— 缓存 60 秒,过期后后台刷新
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }, // 60 秒后标记为 stale
})
// 4. 基于标签的按需重新验证 — 精确控制
const data = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
})2.3 基于时间的重新验证(ISR)
revalidate: 60 的实际工作方式是 Stale-While-Revalidate:
时间线:
0s ← 请求 → 返回新数据,缓存 [FRESH]
30s ← 请求 → 返回缓存(还没过期) [FRESH]
60s ← 过期 → [STALE]
61s ← 请求 → 先返回旧缓存(快!),后台异步请求新数据 [STALE → REVALIDATING]
62s ← 新数据返回 → 更新缓存 [FRESH]
63s ← 请求 → 返回新缓存 [FRESH]
关键点:用户永远不会等待数据刷新。即使缓存过期,用户拿到的也是旧缓存,新数据在后台静默更新。下一个用户才会看到新数据。
2.4 基于标签的按需重新验证
比定时更精确——数据变化时立即刷新缓存:
// 1. 给 fetch 打标签
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
})
return res.json()
}
async function getPost(id: string) {
const res = await fetch(`https://api.example.com/posts/${id}`, {
next: { tags: ['posts', `post-${id}`] },
})
return res.json()
}// 2. 数据变更时按标签刷新
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost(data: FormData) {
await db.insert(posts).values({ ... })
revalidateTag('posts') // 刷新所有带 'posts' 标签的缓存
}
export async function updatePost(id: string, data: FormData) {
await db.update(posts).set({ ... }).where(eq(posts.id, id))
revalidateTag(`post-${id}`) // 只刷新这篇文章
revalidateTag('posts') // 同时刷新列表
}2.5 路径级别的重新验证
import { revalidatePath } from 'next/cache'
// 刷新特定页面
revalidatePath('/blog')
// 刷新特定页面及其所有子路由
revalidatePath('/blog', 'layout')
// 刷新动态路由的某个参数
revalidatePath('/blog/[slug]', 'page')3. Full Route Cache
3.1 原理
Full Route Cache 在构建时把静态路由渲染为 HTML 和 RSC Payload,存储在 CDN 上。用户请求时直接返回预渲染的内容——不需要运行任何服务端代码。
什么路由会被静态渲染? 如果一个路由:
- 没有
cookies()、headers()、searchParams等动态函数 - 所有
fetch()都是force-cache或有revalidate - 没有
export const dynamic = 'force-dynamic'
那么它就是静态的,会被纳入 Full Route Cache。
3.2 静态 vs 动态渲染
// ✅ 静态渲染 — 构建时生成,CDN 缓存
// app/about/page.tsx
export default function AboutPage() {
return <div>关于我们</div>
}
// ✅ 静态渲染 + ISR — 构建时生成,定期刷新
// app/blog/page.tsx
export const revalidate = 3600 // 每小时重新验证
export default async function BlogPage() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 },
})
return <PostList posts={posts} />
}
// ❌ 动态渲染 — 每次请求都重新渲染(不进入 Full Route Cache)
// app/dashboard/page.tsx
import { cookies } from 'next/headers'
export default async function DashboardPage() {
const session = (await cookies()).get('session') // 使用了 cookies() → 强制动态
const data = await getUserData(session?.value)
return <Dashboard data={data} />
}3.3 Route Segment 配置
在 page.tsx 或 layout.tsx 中导出配置来控制缓存行为:
// 强制动态渲染(退出 Full Route Cache)
export const dynamic = 'force-dynamic'
// 强制静态渲染(如果用了动态函数会报错)
export const dynamic = 'force-static'
// 设置整个路由的 revalidate 时间
export const revalidate = 60
// 设置请求超时(ISR 场景)
export const maxDuration = 303.4 generateStaticParams
对于动态路由,generateStaticParams 告诉 Next.js 在构建时预渲染哪些参数:
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await db.query.posts.findMany({
columns: { slug: true },
})
return posts.map((post) => ({
slug: post.slug,
}))
}
// 不在列表中的 slug 会按需渲染并缓存(默认行为)
// 要禁止这种回退行为:
export const dynamicParams = false // 未预渲染的 slug 返回 4044. Router Cache(客户端)
4.1 原理
Router Cache 在浏览器内存中缓存访问过的路由的 RSC Payload。当用户点击链接导航时:
- 先检查 Router Cache——如果有,直接渲染(瞬间切换)
- 如果没有或过期,发请求获取新的 RSC Payload
这就是为什么 Next.js 应用页面切换感觉很快——大部分切换不需要网络请求。
4.2 缓存时效
| 路由类型 | 缓存时间 | 说明 |
|---|---|---|
| 静态路由 | 5 分钟 | <Link prefetch> 预获取的路由 |
| 动态路由 | 30 秒 | 包含 cookies()/headers() 的路由 |
| 预获取的路由 | 5 分钟 | <Link> 默认会预获取可见链接 |
4.3 手动刷新
'use client'
import { useRouter } from 'next/navigation'
export function RefreshButton() {
const router = useRouter()
return (
<button onClick={() => router.refresh()}>
刷新数据
</button>
)
}router.refresh() 会清除当前路由的 Router Cache,重新向服务端请求数据。这不会清除 Data Cache 或 Full Route Cache——它只是让客户端重新获取最新的服务端渲染结果。
4.4 Link 预获取控制
import Link from 'next/link'
// 默认:视口内的 Link 会被自动预获取
<Link href="/about">关于</Link>
// 禁用预获取(不常访问的页面)
<Link href="/terms" prefetch={false}>条款</Link>
// 完整预获取(预获取整个页面,而非只是共享 layout)
<Link href="/pricing" prefetch={true}>价格</Link>5. Request Memoization
5.1 原理
同一次服务端渲染中,如果多个 Server Component 调用了相同的 fetch(),只会实际执行一次,后续调用返回第一次的结果。
// 这两个组件在同一个页面中
// page.tsx 和 sidebar.tsx 都调用了 getUser()
async function getUser() {
// 这个 fetch 在一次渲染中只执行一次
const res = await fetch('https://api.example.com/user')
return res.json()
}
// app/dashboard/page.tsx
export default async function DashboardPage() {
const user = await getUser() // 第 1 次调用 → 实际 fetch
return <main>Welcome, {user.name}</main>
}
// components/sidebar.tsx — 也在 dashboard layout 中使用
export async function Sidebar() {
const user = await getUser() // 第 2 次调用 → 返回 memoized 结果
return <aside>{user.avatar}</aside>
}5.2 适用范围
- 只适用于
fetch():db.query()等直接数据库调用不会被 memoize - 只在单次渲染内:不同请求之间不共享
- 只匹配 URL 和 options:相同 URL + 相同 options 才会命中
对于非 fetch() 的数据请求,用 React 的 cache() 函数手动实现相同效果:
import { cache } from 'react'
import { db } from '@/lib/db'
// 用 cache() 包装数据库查询
export const getUser = cache(async (userId: string) => {
return db.query.users.findFirst({
where: eq(users.id, userId),
})
})6. 'use cache' 指令(实验性)
6.1 概述
'use cache' 是 Next.js 实验性的缓存指令,比 fetch 级别的缓存更灵活——可以缓存整个函数或整个组件的输出:
// next.config.ts — 需要启用实验性功能
const nextConfig: NextConfig = {
experimental: {
dynamicIO: true,
},
}6.2 函数级缓存
'use cache'
import { db } from '@/lib/db'
export async function getPopularPosts() {
// 整个函数的返回值被缓存
const posts = await db.query.posts.findMany({
orderBy: desc(posts.views),
limit: 10,
})
return posts
}6.3 配合 cacheLife 和 cacheTag
import { cacheLife, cacheTag } from 'next/cache'
async function getPost(slug: string) {
'use cache'
cacheLife('hours') // 缓存 1 小时
cacheTag(`post-${slug}`) // 打标签,支持按需刷新
return db.query.posts.findFirst({
where: eq(posts.slug, slug),
})
}cacheLife 预定义了几种时间策略:
| 名称 | stale | revalidate | expire |
|---|---|---|---|
'seconds' | 0 | 1s | 60s |
'minutes' | 5min | 1min | 1h |
'hours' | 5min | 1h | 1d |
'days' | 5min | 1d | 1w |
'weeks' | 5min | 1w | 30d |
'max' | 5min | 30d | 365d |
7. 缓存调试与常见问题
7.1 "数据不更新"排查清单
这是 Next.js 缓存最常见的问题。按顺序排查:
- Router Cache:客户端缓存了旧数据 → 试试硬刷新(Ctrl+Shift+R)或
router.refresh() - Full Route Cache:构建时的静态页面没更新 → 检查是否需要
revalidatePath - Data Cache:
fetch缓存了旧响应 → 检查revalidate配置或revalidateTag - CDN Cache:部署平台的 CDN 缓存了旧内容 → 检查 CDN 缓存头
7.2 开发环境 vs 生产环境
缓存行为在开发和生产环境中不同:
| 行为 | 开发环境(dev) | 生产环境(build + start) |
|---|---|---|
| Data Cache | 默认不缓存 | 根据配置缓存 |
| Full Route Cache | 不缓存(每次渲染) | 构建时生成 |
| Router Cache | 正常工作 | 正常工作 |
这意味着开发时看到数据正常更新,上线后可能"卡住"——因为生产环境的缓存更积极。始终在 pnpm build && pnpm start 下测试缓存行为。
7.3 缓存策略选择指南
| 数据类型 | 推荐策略 | 示例 |
|---|---|---|
| 永不变化 | force-cache(永久缓存) | 关于页面、法律条款 |
| 很少变化 | revalidate: 3600(1小时) | 博客文章列表、产品目录 |
| 经常变化 | revalidateTag + 按需刷新 | 评论、点赞数 |
| 实时数据 | no-store(不缓存) | 仪表盘、聊天消息 |
| 用户个性化 | no-store + 客户端状态 | 购物车、通知 |
本章小结
- Data Cache:缓存
fetch()响应,用revalidate控制时间失效,用revalidateTag精确失效 - Full Route Cache:构建时预渲染静态页面到 CDN,用
revalidatePath/revalidateTag刷新 - Router Cache:客户端内存缓存访问过的路由,
router.refresh()手动刷新 - Request Memoization:同一次渲染内自动去重
fetch(),非 fetch 用cache()包装 'use cache':实验性的函数级缓存指令,配合cacheLife和cacheTag使用- 排查技巧:数据不更新时按 Router Cache → Full Route Cache → Data Cache → CDN 顺序排查