6月5日 21:51

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'每次请求都获取实时数据(用户信息、支付)
ISRnext: { 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 级别的设置。

手动失效缓存

typescript
import { 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 的缓存失效 }

revalidateTagrevalidatePath 更灵活——一个 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 没有执行请求记忆去重正常行为,同一周期只请求一次
标签:Next.js