缓存策略体系

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.tsxlayout.tsx 中导出配置来控制缓存行为:

// 强制动态渲染(退出 Full Route Cache)
export const dynamic = 'force-dynamic'
 
// 强制静态渲染(如果用了动态函数会报错)
export const dynamic = 'force-static'
 
// 设置整个路由的 revalidate 时间
export const revalidate = 60
 
// 设置请求超时(ISR 场景)
export const maxDuration = 30

3.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 返回 404

4. Router Cache(客户端)

4.1 原理

Router Cache 在浏览器内存中缓存访问过的路由的 RSC Payload。当用户点击链接导航时:

  1. 先检查 Router Cache——如果有,直接渲染(瞬间切换)
  2. 如果没有或过期,发请求获取新的 RSC Payload

这就是为什么 Next.js 应用页面切换感觉很快——大部分切换不需要网络请求。

4.2 缓存时效

路由类型缓存时间说明
静态路由5 分钟&lt;Link prefetch&gt; 预获取的路由
动态路由30 秒包含 cookies()/headers() 的路由
预获取的路由5 分钟&lt;Link&gt; 默认会预获取可见链接

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——它只是让客户端重新获取最新的服务端渲染结果。

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 预定义了几种时间策略:

名称stalerevalidateexpire
'seconds'01s60s
'minutes'5min1min1h
'hours'5min1h1d
'days'5min1d1w
'weeks'5min1w30d
'max'5min30d365d

7. 缓存调试与常见问题

7.1 "数据不更新"排查清单

这是 Next.js 缓存最常见的问题。按顺序排查:

  1. Router Cache:客户端缓存了旧数据 → 试试硬刷新(Ctrl+Shift+R)或 router.refresh()
  2. Full Route Cache:构建时的静态页面没更新 → 检查是否需要 revalidatePath
  3. Data Cachefetch 缓存了旧响应 → 检查 revalidate 配置或 revalidateTag
  4. 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':实验性的函数级缓存指令,配合 cacheLifecacheTag 使用
  • 排查技巧:数据不更新时按 Router Cache → Full Route Cache → Data Cache → CDN 顺序排查