Next.js 缓存全解析:四层缓存机制和失效策略实战
Next.js 的缓存机制是它的性能杀手锏,也是最容易让人困惑的部分。一个 fetch 请求可能被缓存 5 秒也可能永久缓存,取决于一行配置。搞不清缓存层级,就会出现"数据改了但页面没更新"的诡异问题。
四层缓存,各有各的失效机制
shell请求进来 → 请求记忆(同一渲染周期内的去重) → 数据缓存(fetch 结果持久化) → 路由缓存(Router Cache,客户端缓存) → 完整路由缓存(构建时静态化)
从上到下,缓存粒度从细到粗。任何一层命中都不会继续往下查。
第一层:请求记忆(Request Memoization)
同一个 Server Component 渲染周期内,多次 fetch 同一个 URL 只会真正发一次请求:
typescript// 这两个 fetch 在同一个渲染周期只会发一次网络请求 async function Layout() { const user = await fetch('/api/user') // 真正请求 // ... } async function Page() { const user = await fetch('/api/user') // 命中记忆,不请求 // ... }
这是 React 的自动去重机制,不需要你做任何配置。只在 Server Component 的单次渲染内有效——渲染完成后记忆清除,下次请求重新获取。
第二层:数据缓存(Data Cache)
这是最容易出问题的缓存层。Next.js 扩展了 fetch API,默认行为是永久缓存:
typescript// 默认:永久缓存(等效 force-cache) const data = await fetch('https://api.example.com/data') // 和上面等价 const data = await fetch('https://api.example.com/data', { cache: 'force-cache' })
是的,你没看错——一次 fetch 的结果会被永久缓存,除非你主动让它失效。
控制缓存行为
typescript// 不缓存,每次都请求 const data = await fetch('https://api.example.com/data', { cache: 'no-store' }) // 缓存 60 秒后重新验证 const data = await fetch('https://api.example.com/data', { next: { revalidate: 60 } })
三种策略对比
| 策略 | 配置 | 行为 | 适用场景 |
|---|---|---|---|
| 静态 | cache: 'force-cache' | 构建时获取,永久缓存 | 不变的数据(配置、字典) |
| 动态 | cache: 'no-store' | 每次请求都获取 | 实时数据(用户信息、支付) |
| ISR | next: { revalidate: 60 } | 缓存 60 秒,到期后台重新验证 | 经常更新但不需实时的数据 |
ISR 的工作原理
revalidate: 60 不是到期立刻更新——而是到期后第一个请求仍然返回缓存旧数据,同时后台触发重新验证。下一个请求就能拿到新数据。这叫 stale-while-revalidate,用户永远不会等到过期数据的重新获取。
按路由段配置 revalidate
不想在每个 fetch 里写 revalidate?在页面级统一设置:
typescript// app/products/page.tsx export const revalidate = 3600 // 整个页面 1 小时重新验证 export default async function ProductsPage() { const products = await fetch('https://api.example.com/products') // ... }
页面级 revalidate 会覆盖 fetch 级别的设置。
手动失效缓存
typescriptimport { revalidatePath, revalidateTag } from 'next/cache' // 失效整个路由 revalidatePath('/products') // 按 tag 失效(更精确) const data = await fetch('https://api.example.com/products', { next: { tags: ['products'] } }) // 数据变更时清除 tag async function updateProduct() { await db.updateProduct(...) revalidateTag('products') // 所有带 products tag 的缓存失效 }
revalidateTag 比 revalidatePath 更灵活——一个 tag 可以跨多个页面和组件,清除一次全部失效。
第三层:路由缓存(Router Cache)
这是客户端缓存,存在浏览器内存中。用户在页面间导航时,Next.js 会缓存已访问路由的 RSC Payload,返回时不需要重新请求服务端。
typescript// 在 layout.tsx 中配置 export const experimental = { staleTimes: { dynamic: 30, // 动态路由缓存 30 秒 static: 300, // 静态路由缓存 5 分钟 }, }
路由缓存的失效
- 用户刷新页面 → 缓存失效
- 调用
router.refresh()→ 缓存失效 revalidatePath/revalidateTag→ 对应缓存失效
常见问题:用户在 A 页面修改了数据,切到 B 页面,再切回 A 页面——看到的还是旧数据。这就是路由缓存在作怪。解决:修改数据后调用 router.refresh()。
typescript'use client' import { useRouter } from 'next/navigation' function UpdateButton() { const router = useRouter() async function handleClick() { await updateData() router.refresh() // 刷新当前路由,清除客户端缓存 } return <button onClick={handleClick}>更新</button> }
第四层:完整路由缓存(Full Route Cache)
构建时,Next.js 会把静态路由的 RSC Payload 和 HTML 都生成好,部署后直接从 CDN 返回——根本不走 Node.js 服务器。
typescript// 默认行为:构建时静态生成 export default async function Page() { const data = await fetch('https://api.example.com/static-data') // force-cache return <div>{data}</div> }
什么时候路由变成动态的
以下任一条件满足,路由就不会被静态生成:
fetch使用了cache: 'no-store'fetch使用了next: { revalidate: 0 }- 使用了
cookies()、headers()等动态 API - 页面导出了
export const dynamic = 'force-dynamic'
typescript// 强制动态渲染 export const dynamic = 'force-dynamic'
Server Actions 与缓存失效
Server Actions 是触发缓存失效的最佳位置——数据变更和缓存失效在同一处代码:
typescript// app/products/actions.ts 'use server' import { revalidateTag } from 'next/cache' export async function createProduct(formData: FormData) { await db.product.create({ data: { name: formData.get('name') } }) revalidateTag('products') // 创建后立刻失效 }
调试缓存
开发模式和生产模式的缓存行为完全不同——开发模式下几乎所有缓存都被禁用。缓存问题只在生产环境中复现。
bash# 生产构建 npm run build && npm start # 查看每个路由的渲染策略 # 构建输出会显示: # ○ /products (静态) # ● /products/[id] (动态) # ƒ /api/products (动态)
符号含义:○ 静态生成,● 服务端渲染,ƒ 动态路由。
缓存策略速查
| 数据特征 | fetch 配置 | 路由类型 |
|---|---|---|
| 不变数据 | cache: 'force-cache' | 静态 |
| 偶尔更新 | next: { revalidate: 3600, tags: ['xxx'] } | ISR |
| 频繁更新 | next: { revalidate: 60 } | ISR |
| 实时数据 | cache: 'no-store' | 动态 |
| 用户相关 | cookies() + cache: 'no-store' | 动态 |
| 问题 | 原因 | 解决 |
|---|---|---|
| 数据更新了页面没变 | 数据缓存未失效 | revalidateTag / revalidatePath |
| 返回上一页看到旧数据 | 路由缓存 | router.refresh() |
| 开发环境正常生产不对 | 开发模式禁用缓存 | 生产构建调试 |
| fetch 没有执行 | 请求记忆去重 | 正常行为,同一周期只请求一次 |