Next.js 缓存体系:多层缓存的深度解析

深入解析 Next.js 的多层缓存体系:Full Route Cache、Data Cache、Router Cache 的关系,fetch 请求的 cache 选项,以及 revalidatePath 和 revalidateTag 缓存失效策略。

为什么需要理解缓存体系

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 时:

  1. Router Cache 检查:内存中是否有 /dashboard 的 RSC Payload?

    • 有:直接渲染,跳到步骤 5
    • 无:继续
  2. Full Route Cache 检查:磁盘/Memory 中是否有 /dashboard 的预渲染 HTML?

    • 有:返回 HTML,继续步骤 4
    • 无:需要渲染
  3. 渲染和 Data Cache

    • 服务端渲染页面
    • fetch 请求结果存入 Data Cache
    • 结果存入 Full Route Cache
  4. HTML 返回浏览器:React 从 RSC Payload 客户端水合

  5. 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:包括 methodheadersbody
  • 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。只能通过:

  1. 时间过期:默认 5 分钟(基于 Last-Modified / Age 计算)
  2. 用户导航:用户访问新页面时,对应的缓存条目会随时间失效
  3. 刷新(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') 时:

  1. Full Route Cache/products/1 被标记为失效
  2. Data Cache 中所有与 /products/1 相关的 fetch 请求被标记为失效
  3. 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)在用户浏览器中,服务端无法直接操作,只能等待时间过期或用户刷新。


延展阅读