为什么需要理解缓存体系
Next.js 以"零配置高性能"为目标,内置了复杂的缓存系统。如果你不理解它的工作原理,可能会遇到:
- 页面数据不更新(缓存未正确失效)
- 开发环境和生产环境行为不一致
- 部署后性能反而下降
- 难以诊断的数据 stale 问题
理解缓存体系不仅是面试必备,也能帮助你在实际项目中避免这些陷阱。
一、Next.js 缓存全景图
Next.js 的缓存系统由四层组成,每一层有不同的职责和失效机制:
┌─────────────────────────────────────────────────────┐
│ Request → Response │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ Router Cache (Client) │
│ 客户端内存缓存:已访问页面的 RSC Payload │
│ 失效:时间过期 或 navigate │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ Full Route Cache (Server) │
│ 服务端缓存:预渲染的 HTML 和 RSC Payload │
│ 失效:revalidatePath / revalidateTag │
│ 或时间过期(revalidate) │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ Data Cache (Server) │
│ fetch 请求的响应缓存(按 URL + options 索引) │
│ 失效:revalidateTag 或时间过期 │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ Source Data │
│ 数据库 / CMS / 外部 API │
└─────────────────────────────────────────────────────┘
1.1 四层缓存的角色
| 缓存层 | 位置 | 内容 | 失效方式 |
|---|---|---|---|
| Router Cache | 客户端内存 | 已访问页面的 RSC Payload | 时间过期、navigate |
| Full Route Cache | 服务端磁盘/Memory | 预渲染的 HTML + RSC Payload | revalidatePath/revalidateTag |
| Data Cache | 服务端 Memory | fetch 响应的原始数据 | revalidateTag、时间过期 |
| 源数据 | 外部 | 数据库、CMS、API | 外部决定 |
1.2 缓存命中流程
当用户请求 /dashboard 时:
-
Router Cache 检查:内存中是否有
/dashboard的 RSC Payload?- 有:直接渲染,跳到步骤 5
- 无:继续
-
Full Route Cache 检查:磁盘/Memory 中是否有
/dashboard的预渲染 HTML?- 有:返回 HTML,继续步骤 4
- 无:需要渲染
-
渲染和 Data Cache:
- 服务端渲染页面
- fetch 请求结果存入 Data Cache
- 结果存入 Full Route Cache
-
HTML 返回浏览器:React 从 RSC Payload 客户端水合
-
Router Cache 更新:本次 RSC Payload 存入 Router Cache
二、Full Route Cache(完整路由缓存)
2.1 什么是 Full Route Cache
Full Route Cache 存储了 Next.js 在构建时(或运行时重新验证时)预渲染的页面 HTML 和对应的 RSC Payload。这是实现 SSG 和 ISR 的基础。
// app/products/[id]/page.tsx
// 这个页面会被预渲染并存储在 Full Route Cache 中
export async function generateStaticParams() {
const products = await getProducts();
return products.map(p => ({ id: p.id }));
}
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
return <ProductDetail product={product} />;
}
2.2 Full Route Cache 的存储位置
- 构建时:存储在
.next/server/cache/目录下的文件 - 运行时(ISR):在内存中(如果使用
revalidate),或磁盘上(如果使用 On-demand revalidation)
2.3 何时命中 Full Route Cache
请求到达
↓
dynamic = 'force-dynamic' ?
→ 是:不使用 Full Route Cache,每次 SSR
→ 否:继续
revalidate 设置了?
→ 是:检查缓存是否过期
→ 否:直接返回缓存(SSG 场景)
三、Data Cache(数据缓存)
3.1 什么是 Data Cache
Data Cache 是 Next.js 对 fetch 请求响应的缓存。每个唯一的 fetch 请求(由 URL + fetch options 决定)都会被缓存。
// 这个 fetch 的结果会被 Data Cache 缓存
const data = await fetch('https://api.example.com/products', {
cache: 'force-cache', // 默认值
next: { revalidate: 60 } // 60 秒后过期
});
3.2 Data Cache 的 key
Data Cache 的 key 由以下因素决定:
- URL(完整的 query string)
- fetch options:包括
method、headers、body - revalidate` 配置
同一个 URL 但不同的 cache 选项会产生不同的缓存条目。
3.3 fetch 的 cache 选项
// 强制缓存(默认)— 除非设置了 revalidate,否则永不过期
const data = await fetch(url, { cache: 'force-cache' });
// 不缓存 — 每次请求都重新获取
const data = await fetch(url, { cache: 'no-store' });
// 时间基础重新验证
const data = await fetch(url, { next: { revalidate: 3600 } }); // 1 小时
// 标签基础重新验证(配合 revalidateTag)
const data = await fetch(url, { next: { tags: ['products'] } });
3.4 非 fetch 数据源的缓存
如果使用 ORM(如 Prisma)或直接数据库查询,可以用 unstable_cache 包装:
import { unstable_cache } from 'next/cache';
const getProductWithCache = unstable_cache(
async (id: string) => {
return db.product.findUnique({ where: { id } });
},
['product'], // cache key 的组成部分
{
revalidate: 60,
tags: ['product']
}
);
四、Router Cache(路由缓存)
4.1 什么是 Router Cache
Router Cache 是客户端内存缓存,存储了用户已访问页面的 RSC Payload。它的目的是加速后续导航(back/forward navigation 和 link 点击)。
// 点击 <Link href="/about"> 时:
// 如果 /about 在 Router Cache 中,不需要网络请求
// 直接使用缓存的 RSC Payload 渲染
<Link href="/about">About</Link>
4.2 Router Cache 的失效机制
Router Cache 的失效比较特殊:服务端无法直接清除客户端的 Router Cache。只能通过:
- 时间过期:默认 5 分钟(基于 Last-Modified / Age 计算)
- 用户导航:用户访问新页面时,对应的缓存条目会随时间失效
- 刷新(refresh):用户手动刷新页面时,Router Cache 被清除,重新从服务端获取
// 强制页面重新验证(但不能直接清除 Router Cache)
// 这会导致 Full Route Cache 和 Data Cache 失效
revalidatePath('/dashboard');
// 用户下次访问 /dashboard 时会获取新数据
// 但如果 Router Cache 中有旧数据且未过期,
// 用户通过 link 点击访问可能看到旧数据
4.3 router.refresh()
'use client';
import { useRouter } from 'next/navigation';
function RefreshButton() {
const router = useRouter();
return (
<button onClick={() => router.refresh()}>
刷新数据
</button>
);
}
router.refresh() 会清除 Router Cache 并重新从服务端获取当前页面的 RSC Payload。这不会触发页面重新加载(没有 full navigation),只是更新页面数据。
五、缓存失效策略
5.1 revalidatePath
revalidatePath 使特定路径的所有缓存失效:
import { revalidatePath } from 'next/cache';
await db.product.update({
where: { id: productId },
data: { price: newPrice }
});
// 使 /products/[id] 的 Full Route Cache 失效
revalidatePath(`/products/${productId}`);
// 也支持通配符
revalidatePath('/products/[id]', 'page'); // 精确匹配 page 路由
5.2 revalidateTag
revalidateTag 使所有带有特定标签的 Data Cache 和 Full Route Cache 失效:
import { revalidateTag } from 'next/cache';
await db.product.update({
where: { id: productId },
data: { price: newPrice }
});
// 使所有标记为 'products' 的 fetch 缓存失效
revalidateTag('products');
// 带有标签的 fetch
const products = await fetch('https://api.example.com/products', {
next: { tags: ['products'] }
});
5.3 失效的传播顺序
当你调用 revalidatePath('/products/1') 时:
- Full Route Cache 中
/products/1被标记为失效 - Data Cache 中所有与
/products/1相关的 fetch 请求被标记为失效 - Router Cache 中
/products/1不会被清除(只能等待时间过期)
这意味着即使用了 revalidatePath,用户如果通过 link 点击回到 /products/1,如果 Router Cache 中还有这个页面的数据且未过期,仍可能看到旧数据。这就是为什么有时需要配合 router.refresh() 使用。
5.4 On-demand Revalidation 的典型场景
CMS 更新文章时:
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const { slug, type } = await request.json();
// CMS webhook 调用这个 endpoint
// 立即使相关缓存失效
if (type === 'post') {
revalidatePath(`/blog/${slug}`); // 精确路径
revalidatePath('/blog'); // 列表页
revalidateTag('posts'); // 所有标记为 'posts' 的 fetch
}
return Response.json({ revalidated: true });
}
六、缓存与 ISR 的关系
ISR(Incremental Static Regeneration)是基于 Full Route Cache 和 Data Cache 实现的:
// app/blog/[slug]/page.tsx
// 每小时重新验证一次
export const revalidate = 3600;
export default async function BlogPost({ params }) {
// 这个 fetch 也会被缓存 1 小时
const post = await fetch(`https://cms.example.com/posts/${params.slug}`, {
next: { revalidate: 3600 }
});
return <Article post={post} />;
}
当 1 小时过后,第一个请求会触发重新渲染:新数据被获取、缓存被更新、旧 HTML 被替换。
七、面试高频问题
Q1: Full Route Cache 和 Data Cache 的区别是什么?
Full Route Cache 缓存的是渲染结果(HTML + RSC Payload),用于快速返回预渲染页面。Data Cache 缓存的是fetch 响应数据,用于避免重复的网络请求和计算。Full Route Cache 依赖 Data Cache——渲染时使用 Data Cache 中的数据生成 HTML。
Q2: 为什么页面数据不更新?
最常见的原因:使用了 revalidatePath 但用户 Router Cache 中还有旧数据。或者 revalidate 时间未到。解决方案:确保 CMS/webhook 调用了正确的失效 API,或缩短 revalidate 时间,或引导用户手动刷新页面。
Q3: revalidatePath 和 revalidateTag 怎么选?
revalidatePath 适合精确控制——你知道具体哪个 URL 需要失效。revalidateTag 适合批量失效——当多个 fetch 请求使用相同的标签时,用一个 revalidateTag 可以使它们全部失效。
Q4: 服务端缓存和客户端缓存的区别?
服务端缓存(Full Route Cache、Data Cache)是部署环境控制的,可以主动失效。客户端缓存(Router Cache)在用户浏览器中,服务端无法直接操作,只能等待时间过期或用户刷新。
延展阅读
- Next.js 官方文档 — Caching — 最权威的缓存文档
- Next.js 官方文档 — revalidatePath — API 文档
- Next.js 官方文档 — revalidateTag — API 文档
- Vercel 博客 — Next.js Cache Architecture Deep Dive — 缓存架构深度解析