切换语言
切换主题

Next.js 缓存机制完全指南:掌握 revalidate 的正确使用时机

凌晨一点,我盯着屏幕上那个该死的”旧数据”,已经刷新了第20次。明明10分钟前在数据库里手动改了标题,但页面就是不更新。打开代码,看到 revalidate: 60 写得清清楚楚。怒删,改成 revalidate: 10。重启服务器。刷新。还是旧的。

这种崩溃感,你懂吗?

说实话,Next.js 的缓存机制可能是整个框架里最让人抓狂的部分。四层缓存,三个 revalidate 方法,还有个从 14 到 15 的破坏性变更。你以为设置个 revalidate: 60 就完事了?不,可能是 Router Cache 在捣鬼,可能是 Full Route Cache 没失效,也可能你根本就在开发环境测试。

这篇文章不会跟你讲”缓存是提升性能的重要手段”这种正确的废话。我会用最直白的方式告诉你:

  • Next.js 到底藏了几层缓存,每层管啥
  • revalidatePath、revalidateTag、updateTag 有啥区别,该用哪个
  • 数据不更新时,怎么一层层排查问题

如果你也遇到过数据不更新、revalidate 不生效、不知道该用哪个 API 的困境,那接下来的 12 分钟,可能会帮你省下好几个通宵。

为什么 Next.js 的缓存这么复杂

为什么要搞这么多层?

老实讲,第一次看到 Next.js 有四层缓存时,我的表情大概跟你现在一样。Request Memoization? Full Route Cache? 这都是啥?能不能简单点?

但冷静下来想想,每层缓存其实都在解决不同环境下的性能问题:

  • 你的组件树里,10个组件都要获取用户信息,总不能发10次请求吧?
  • 你的博客文章列表,一天也不会更新几次,总不能每次访问都重新渲染吧?
  • 用户点了返回键,总不能让他等页面重新加载吧?

每层缓存都有自己的职责。问题是,它们会互相影响。这也是为什么你改了数据,却不知道该清哪个缓存。

Next.js 14 vs 15: 一场缓存革命

2024年底,Next.js 15 搞了个大新闻:默认不再缓存 fetch 请求。

以前(14):

fetch(url) // 默认缓存,等于 cache: 'force-cache'

现在(15):

fetch(url) // 默认不缓存,等于 cache: 'no-store'

这改动导致很多人升级后发现性能暴跌,因为原来自动缓存的数据现在都不缓存了。Vercel 论坛上骂声一片,但官方的理由是:“显式优于隐式,缓存应该是开发者主动选择的,而不是默认行为。”

听起来很有道理。但对已有项目来说,这就是个破坏性变更。

四层缓存全景图

简单来说,数据从服务器到用户浏览器,会经过这四层:

  1. Request Memoization(请求记忆化)
    作用域:单次请求的渲染周期
    管理者:React

  2. Data Cache(数据缓存)
    作用域:服务端,跨请求持久化
    管理者:Next.js

  3. Full Route Cache(完整路由缓存)
    作用域:服务端,静态路由
    管理者:Next.js

  4. Router Cache(路由器缓存)
    作用域:客户端浏览器内存
    管理者:Next.js

数据流大概是这样的:

用户访问 → Router Cache(客户端)→ Full Route Cache(服务端)

                            Data Cache → Request Memoization → 数据源

你的 revalidate 主要影响的是 Data Cache 和 Full Route Cache。而 Router Cache 需要用 router.refresh() 或硬刷新才能清除。

这就是为什么有时候你明明 revalidate 了,但客户端刷新页面还是看到旧数据——因为 Router Cache 还在。

下一章,我会一层层拆解这四个缓存,告诉你每层到底在干什么,什么时候会生效,什么时候会失效。

四层缓存机制详解

Request Memoization(请求记忆化)

这是啥?

这其实是 React 18 的特性,不是 Next.js 专门搞的。简单说,在一次渲染周期里,如果多个组件发了相同的 GET 请求,React 会自动帮你合并成一次。

举个例子:

// app/page.tsx
async function UserProfile() {
  const user = await fetch('https://api.example.com/user/123')
  return <div>{user.name}</div>
}

async function UserAvatar() {
  const user = await fetch('https://api.example.com/user/123')  // 相同的请求
  return <img src={user.avatar} />
}

export default function Page() {
  return (
    <>
      <UserProfile />
      <UserAvatar />
    </>
  )
}

这两个组件都请求了同一个 URL,但实际上只会发一次请求。React 会记住第一次的结果,第二次直接用缓存。

生命周期超短

这个缓存只在单次渲染过程中有效。渲染结束,缓存就清空了。下次用户刷新页面,又是全新的请求。

注意事项

  • 只对服务端组件(Server Component)有效
  • 只对 GET 请求有效,POST、PUT 这些不会记忆化
  • 开发环境可能看不到效果,因为每次改代码都触发重新渲染

什么时候会用到?

你可能根本不需要关心它。这是 React 自动帮你做的优化,无法手动控制。我提到它只是为了让你知道:如果你在多个组件里写了相同的 fetch,不用担心会发多次请求。

Data Cache(数据缓存)

这才是重点

Data Cache 是 Next.js 缓存机制的核心。它会把 fetch 请求的结果存到服务器的文件系统里,跨请求、跨用户、跨部署持久化。

Next.js 14 vs 15 的天壤之别

Next.js 14:

fetch('https://api.example.com/posts')
// 等同于
fetch('https://api.example.com/posts', { cache: 'force-cache' })
// 结果:数据会永久缓存,除非手动 revalidate

Next.js 15:

fetch('https://api.example.com/posts')
// 等同于
fetch('https://api.example.com/posts', { cache: 'no-store' })
// 结果:每次都重新请求,不缓存

如果你要缓存数据(Next.js 15)

方式1:单个请求设置

fetch('https://api.example.com/posts', {
  cache: 'force-cache',
  next: { revalidate: 3600 }  // 1小时后重新验证
})

方式2:整个路由设置

// app/blog/page.tsx
export const revalidate = 3600

export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts')
  // ...
}

什么时候失效?

Data Cache 的失效时机有三个:

  1. 设置的 revalidate 时间到了(比如3600秒)
  2. 手动调用 revalidatePath()revalidateTag()
  3. 重新部署应用

多个 fetch 的 revalidate 时间不一致怎么办?

如果一个页面有多个 fetch,每个设置的 revalidate 时间不同,Next.js 会取最短的那个作为整个页面的重新验证时间。

async function Page() {
  const posts = await fetch('...', { next: { revalidate: 60 } })    // 60秒
  const user = await fetch('...', { next: { revalidate: 3600 } })  // 1小时

  // 实际上,整个页面会按 60 秒重新验证
}

Full Route Cache(完整路由缓存)

HTML 级别的缓存

如果说 Data Cache 缓存的是数据,那 Full Route Cache 缓存的就是整个页面的 HTML 和 RSC Payload(React Server Component 的序列化数据)。

什么时候会缓存?

只有静态渲染的路由才会被缓存。什么是静态渲染?就是构建时就能确定内容的页面。

反过来,如果你的页面用了这些东西,就会变成动态渲染,不会被缓存:

  • cookies()
  • headers()
  • searchParams
  • 不稳定的函数(如 Math.random()Date.now()

怎么判断页面是静态还是动态?

运行 npm run build,终端会显示:

Route (app)                              Size     First Load JS
┌ ○ /                                    5 kB           87 kB
├ ● /blog                                1 kB           88 kB
└ ƒ /api/user                            0 kB           87 kB

○  (Static)  自动渲染为静态HTML(使用无动态数据)
●  (SSG)     自动生成为静态HTML + JSON(使用 getStaticProps)
ƒ  (Dynamic) 服务器端按需渲染

看到 就是静态,会被缓存。看到 ƒ 就是动态,不会缓存。

强制静态或动态

// 强制静态
export const dynamic = 'force-static'

// 强制动态
export const dynamic = 'force-dynamic'

什么时候失效?

Full Route Cache 的失效时机:

  1. Data Cache 失效时,Full Route Cache 也会失效(因为数据变了,页面也得重新渲染)
  2. 调用 revalidatePath('/blog')
  3. 重新部署应用

Router Cache(路由器缓存)

客户端的小心机

Router Cache 是存在用户浏览器内存里的缓存。当用户访问过一个页面后,Next.js 会把这个页面的内容缓存在客户端,下次用户点”返回”或者跳转到这个页面,直接用缓存,不用重新请求服务器。

还有个骚操作:预取

如果你用了 <Link href="/about">,当这个链接出现在用户视口里时,Next.js 会自动预取 /about 页面的内容,存到 Router Cache 里。等用户真点击时,瞬间就能跳转。

生命周期(Next.js 14)

  • 静态路由:缓存 5 分钟
  • 动态路由:缓存 30 秒

Next.js 15 的变化

Next.js 15 默认不启用 Router Cache(或者说缓存时间非常短)。如果你想启用,需要在 next.config.js 里配置:

// next.config.js
module.exports = {
  experimental: {
    staleTimes: {
      dynamic: 30,      // 动态路由缓存30秒
      static: 180,      // 静态路由缓存180秒
    },
  },
}

什么时候失效?

  • 缓存时间到了
  • 用户硬刷新(Ctrl+Shift+R)
  • 调用 router.refresh()

为什么你的 revalidate 看起来不生效?

很多时候,你在服务端调用了 revalidatePath,数据也确实更新了,但用户刷新页面还是看到旧数据。十有八九是因为 Router Cache 还没失效

解决方法:

  1. 用户硬刷新(但你不能要求用户这么做)
  2. 在更新数据后,调用 router.refresh()(如果是客户端组件)
  3. 缩短 Router Cache 的时间

revalidate 方法全解析

好了,前面讲了四层缓存的原理。现在到了最实用的部分:怎么让缓存失效

Next.js 提供了好几种 revalidate 方法,每种适用场景不同。搞清楚它们的区别,能帮你省下大量调试时间。

时间基础的 revalidate(Time-based)

最常用的方式

时间基础的 revalidate,就是”每隔X秒,自动重新获取数据”。这是 ISR(Incremental Static Regeneration,增量静态再生成)的核心机制。

两种写法

方式1:在 fetch 请求里设置

const res = await fetch('https://api.example.com/posts', {
  next: { revalidate: 3600 }  // 3600秒 = 1小时
})

方式2:在路由文件顶层设置

// app/blog/page.tsx
export const revalidate = 3600

export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts')
  return <PostList posts={posts} />
}

工作原理(ISR)

假设你设置了 revalidate: 3600:

  1. 第一个用户访问页面 → 生成静态HTML,缓存1小时
  2. 接下来1小时内,所有用户看到的都是这个缓存的HTML(超快)
  3. 1小时后,下一个用户访问 → 还是返回旧的HTML(不会等待)
  4. 但同时,Next.js 在后台重新生成新的HTML
  5. 新HTML生成完成后,后续用户看到的就是新内容了

这个机制叫 stale-while-revalidate(在重新验证时返回旧内容)。优点是用户永远不会等待,缺点是总有一个用户看到的是过期数据。

适用场景

  • 博客文章列表(每小时更新几次)
  • 新闻首页(每30分钟更新)
  • 产品目录(每天更新一次)

常见问题1:开发环境不生效

很多人抱怨 revalidate 设置了不生效。第一个要检查的就是:你是在开发环境还是生产环境测试的?

开发环境(npm run dev)下,Next.js 会禁用大部分缓存,每次请求都重新渲染。你必须在生产环境测试:

npm run build
npm start

常见问题2:多个 fetch 的时间不一致

前面提到过,如果一个页面有多个 fetch,revalidate 时间不同,Next.js 会取最小值。但这里有个细节:Data Cache 还是会尊重每个 fetch 自己的时间

async function Page() {
  // 这个请求每60秒重新验证
  const posts = await fetch('...', { next: { revalidate: 60 } })

  // 这个请求每3600秒重新验证
  const user = await fetch('...', { next: { revalidate: 3600 } })
}

页面会每60秒重新渲染,但 user 这个请求的数据缓存会保持1小时。也就是说,前1小时内,每60秒页面会重新渲染,但 user 数据不变;1小时后,user 数据才会更新。

听起来有点绕,但实际上这是合理的设计。

按需 revalidate:revalidatePath

用户触发的更新

时间基础的 revalidate 是定时器,revalidatePath 则是按钮——当特定事件发生时(比如用户发布了新文章),你手动触发缓存失效。

使用方式

// app/actions.ts
'use server'

import { revalidatePath } from 'next/cache'

export async function publishPost(formData) {
  // 发布文章的逻辑
  await db.posts.create({ ... })

  // 让博客列表页的缓存失效
  revalidatePath('/blog')
}

然后在客户端组件里调用:

// app/components/PublishButton.tsx
'use client'

import { publishPost } from '@/app/actions'

export function PublishButton() {
  return (
    <form action={publishPost}>
      <button type="submit">发布文章</button>
    </form>
  )
}

不同的路径类型

revalidatePath 可以指定路径类型:

// 只重新验证 /blog 这一个页面
revalidatePath('/blog', 'page')

// 重新验证 /blog 下的所有页面(包括 /blog/post-1, /blog/post-2)
revalidatePath('/blog', 'layout')

重要:不是立即重新生成

很多人以为调用 revalidatePath 后,页面会立即重新生成。不是的

revalidatePath 只是把缓存标记为失效。真正的重新生成发生在下一次有用户访问这个页面时

这意味着:

  1. 你调用 revalidatePath('/blog')
  2. 缓存被清空
  3. 下一个用户访问 /blog → 这时才重新生成HTML(用户需要等待)
  4. 后续用户看到的就是新内容了

适用场景

  • 用户发布新内容后刷新列表
  • 管理员修改配置后刷新相关页面
  • 用户提交表单后刷新当前页

按需 revalidate:revalidateTag

更灵活的失效控制

revalidatePath 是按路径失效,revalidateTag 则是按标签失效。你可以给数据打上标签,然后批量失效所有使用这个标签的缓存。

使用方式

步骤1:给 fetch 请求打标签

const posts = await fetch('https://api.example.com/posts', {
  next: {
    revalidate: 3600,
    tags: ['posts']  // 打上 'posts' 标签
  }
})

const authors = await fetch('https://api.example.com/authors', {
  next: {
    revalidate: 3600,
    tags: ['posts', 'authors']  // 可以打多个标签
  }
})

步骤2:失效特定标签的缓存

'use server'

import { revalidateTag } from 'next/cache'

export async function publishPost() {
  await db.posts.create({ ... })

  // 所有标记了 'posts' 的数据都会失效
  revalidateTag('posts')
}

profile=“max” 的stale-while-revalidate策略

Next.js 15 推荐使用 profile="max":

revalidateTag('posts', { profile: 'max' })

使用 profile="max" 时:

  1. 标记为过期,但不立即删除缓存
  2. 下次有人访问 → 返回旧数据(fast!)
  3. 同时在后台获取新数据
  4. 新数据准备好后,后续请求返回新数据

这比默认行为要好,因为用户不需要等待。

revalidatePath vs revalidateTag 的区别

对比维度revalidatePathrevalidateTag
失效粒度按路径按数据标签
跨页面只能指定具体路径可以跨多个页面
精细度
使用复杂度简单需要提前规划标签

什么时候用 Tag?

当你的数据会被多个页面使用时,用 Tag 更合适。

举个例子,一个博客系统:

  • 文章列表页 /blog
  • 文章详情页 /blog/[slug]
  • 作者页 /author/[id]
  • 首页的”最新文章”模块

这四个页面都使用了 “文章数据”。如果你用 revalidatePath,就得这样:

revalidatePath('/blog')
revalidatePath('/blog/[slug]')
revalidatePath('/author/[id]')
revalidatePath('/')

但如果你给文章数据打上 posts 标签,只需要:

revalidateTag('posts')

所有使用了这个标签的页面都会失效。省事多了。

新增:updateTag(Next.js 15)

立即失效,而不是延迟失效

updateTag 是 Next.js 15 新增的API,与 revalidateTag 的区别是:立即删除缓存,而不是标记为过期

'use server'

import { updateTag } from 'next/cache'

export async function updateUserProfile(userId, newData) {
  await db.users.update({ where: { id: userId }, data: newData })

  // 立即失效用户数据的缓存
  updateTag(`user-${userId}`)
}

与 revalidateTag 的区别

对比维度revalidateTagupdateTag
失效方式标记为过期,下次访问时在后台更新立即删除缓存
下次访问返回旧数据,后台获取新数据阻塞等待,获取新数据
使用限制可在任何地方使用只能在 Server Actions 中使用
适用场景一般场景,追求速度”读取自己的写入”场景

什么是”读取自己的写入”?

假设用户在个人资料页修改了昵称,点击保存后,页面应该立即显示新昵称,而不是还显示旧的(然后等后台慢慢更新)。

这种场景就用 updateTag:

export async function updateProfile(formData) {
  const userId = getCurrentUserId()

  await db.users.update({
    where: { id: userId },
    data: { nickname: formData.get('nickname') }
  })

  // 立即失效,确保下次读取能看到最新数据
  updateTag(`user-${userId}`)

  revalidatePath('/profile')
}

新增:use cache 指令(Next.js 15)

显式声明缓存

Next.js 15 引入了新的 'use cache' 指令,可以显式标记哪些函数应该被缓存。

使用方式

'use cache'

export async function getPopularPosts() {
  const posts = await db.posts.findMany({
    orderBy: { views: 'desc' },
    take: 10
  })
  return posts
}

配合 cacheTag 使用

import { unstable_cacheTag as cacheTag } from 'next/cache'

'use cache'

export async function getPostsByAuthor(authorId) {
  cacheTag('posts', `author-${authorId}`)

  return await db.posts.findMany({
    where: { authorId }
  })
}

然后可以用 revalidateTag 失效:

revalidateTag(`author-${authorId}`)

为什么需要这个?

在 Next.js 15 中,由于 fetch 默认不缓存,如果你不想每次都重新查询数据库,就需要显式声明缓存。use cache 让你的意图更明确。

常见问题排查指南

理论讲完了。现在是最实用的部分:当缓存出问题时,怎么排查

我会列出最常见的四个问题,每个都附上排查步骤和解决方案。

问题1:revalidate 设置了不生效

症状

你设置了 export const revalidate = 60,但10分钟后访问页面,数据还是旧的。

排查步骤

1. 确认在生产环境测试

这是最常见的误区。开发环境(npm run dev)会禁用大部分缓存。

必须这样测试:

npm run build
npm start

2. 检查路由是否为动态渲染

运行 npm run build,查看终端输出:

Route (app)                Size
├ ○ /blog                  1 kB    ← 静态,会缓存
└ ƒ /profile               2 kB    ← 动态,不会缓存

如果你的路由是 ƒ(动态),那 revalidate 根本不会生效,因为动态路由不缓存。

可能导致动态渲染的代码:

// 这些都会让路由变成动态渲染
import { cookies } from 'next/headers'
import { headers } from 'next/headers'

export default function Page({ searchParams }) {  // 使用了 searchParams
  const cookieStore = cookies()  // 使用了 cookies
  // ...
}

解决方案:

  • 如果不需要动态渲染,移除这些代码
  • 如果需要,就不要期望 revalidate 生效,改用按需 revalidate

3. 检查 Next.js 版本

Next.js 14 和 15 的默认行为不同。如果你刚升级到 15,很多原来缓存的东西现在不缓存了。

解决方案(Next.js 15):

// 显式启用缓存
fetch(url, {
  cache: 'force-cache',
  next: { revalidate: 60 }
})

// 或使用 use cache 指令
'use cache'
export async function getData() {
  // ...
}

4. 检查是否被 Router Cache 干扰

即使服务端数据更新了,客户端的 Router Cache 可能还在缓存旧数据。

解决方案:

  • 硬刷新(Ctrl+Shift+R)
  • 或在 Next.js 15 配置更短的 staleTimes

问题2:数据更新了但页面还是旧数据

症状

你在数据库里手动改了数据,或者调用了 revalidatePath,但刷新页面还是看到旧数据。

排查思路:四层缓存逐层检查

第1层:Router Cache(客户端)

最容易忽略的就是客户端缓存。

快速测试:

  • 按 Ctrl+Shift+R 硬刷新
  • 如果数据更新了,说明是 Router Cache 的问题

解决方案:

'use client'

import { useRouter } from 'next/navigation'

export function RefreshButton() {
  const router = useRouter()

  return (
    <button onClick={() => router.refresh()}>
      刷新
    </button>
  )
}

或者在 next.config.js 配置更短的缓存时间:

module.exports = {
  experimental: {
    staleTimes: {
      dynamic: 0,    // 禁用动态路由缓存
      static: 30,    // 静态路由缓存30秒
    },
  },
}

第2层:Full Route Cache(服务端)

检查路由是否为静态。如果是静态路由,整个HTML都被缓存了。

快速测试:

# 打开新的无痕窗口访问页面
# 如果还是旧数据,说明是服务端缓存

解决方案:

'use server'

import { revalidatePath } from 'next/cache'

export async function updateData() {
  await db.update({ ... })

  // 清除路由缓存
  revalidatePath('/your-page')
}

第3层:Data Cache(服务端)

检查 fetch 请求的配置。

快速测试:

在 fetch 里加上时间戳日志:

const data = await fetch(url)
console.log('Fetched at:', new Date().toISOString())

刷新页面,如果时间戳不变,说明用的是缓存。

解决方案:

方式1:给数据打标签,然后失效

// 获取数据时
const data = await fetch(url, {
  next: { tags: ['my-data'] }
})

// 更新数据时
revalidateTag('my-data')

方式2:暂时禁用缓存进行测试

const data = await fetch(url, {
  cache: 'no-store'  // 不缓存
})

第4层:Request Memoization(服务端)

这层一般不会有问题,因为它只在单次请求中有效。如果前三层都没问题,就不是缓存的问题了,可能是数据源本身的问题。

问题3:revalidatePath vs revalidateTag 该用哪个?

决策树

需要失效缓存
    |
    ├─ 只影响一个页面
    |      → 用 revalidatePath('/specific-page')
    |
    ├─ 影响一个路径下的所有页面
    |      → 用 revalidatePath('/blog', 'layout')
    |
    ├─ 数据被多个不同路径的页面使用
    |      → 用 revalidateTag('your-tag')
    |
    └─ 需要立即失效(用户编辑后立即看到)
           → 用 updateTag('your-tag')(Next.js 15)

实战案例:博客系统

假设你的博客系统有这些页面:

  • 首页:展示最新3篇文章
  • 博客列表页 /blog:所有文章
  • 文章详情页 /blog/[slug]:单篇文章
  • 作者页 /author/[id]:作者的所有文章

使用标签的策略:

// 获取数据时打标签
async function getPosts() {
  return fetch('https://api.example.com/posts', {
    next: {
      revalidate: 3600,
      tags: ['posts']  // 所有文章相关的数据都打这个标签
    }
  })
}

async function getPostBySlug(slug) {
  return fetch(`https://api.example.com/posts/${slug}`, {
    next: {
      revalidate: 3600,
      tags: ['posts', `post-${slug}`]  // 既有通用标签,又有特定标签
    }
  })
}

async function getPostsByAuthor(authorId) {
  return fetch(`https://api.example.com/posts?author=${authorId}`, {
    next: {
      revalidate: 3600,
      tags: ['posts', `author-${authorId}-posts`]
    }
  })
}

发布新文章时:

export async function publishPost(formData) {
  await db.posts.create({ ... })

  // 只需要失效 'posts' 标签
  // 所有使用这个标签的页面都会更新
  revalidateTag('posts')
}

修改特定文章时:

export async function updatePost(slug, newData) {
  await db.posts.update({ where: { slug }, data: newData })

  // 只失效这篇文章相关的缓存
  revalidateTag(`post-${slug}`)

  // 或者,如果你想同时更新列表页
  revalidateTag('posts')
}

问题4:Next.js 14 升级到 15 后缓存不工作了

症状

升级到 Next.js 15 后,原来缓存得好好的数据现在每次都重新请求,性能暴跌。

原因

Next.js 15 的三大缓存默认行为变化:

  1. fetch 默认从 force-cache 变为 no-store
  2. GET 路由处理器默认不缓存
  3. Router Cache 默认不启用

迁移方案

方案1:显式启用缓存(推荐)

// 以前(Next.js 14)
const data = await fetch(url)

// 现在(Next.js 15)- 需要显式声明
const data = await fetch(url, {
  cache: 'force-cache',
  next: { revalidate: 3600 }
})

方案2:使用 use cache 指令

'use cache'

export async function getPostList() {
  const posts = await db.posts.findMany()
  return posts
}

方案3:启用 Router Cache

// next.config.js
module.exports = {
  experimental: {
    staleTimes: {
      dynamic: 30,
      static: 180,
    },
  },
}

对比一下迁移前后:

// Next.js 14 - 隐式缓存
export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts')
  // 自动缓存
}

// Next.js 15 - 显式缓存
'use cache'  // 加上这行

export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts', {
    cache: 'force-cache',  // 或者加上这个
    next: { revalidate: 3600 }
  })
}

我的建议:

别指望一键迁移。仔细审查每个数据请求,有意识地决定哪些需要缓存,哪些不需要。这虽然麻烦,但长期来看更健康——你会清楚知道系统里哪些数据被缓存了,哪些没有。

最佳实践与选择策略

前面讲了这么多,现在总结一下:什么时候用什么缓存策略

缓存策略选择流程图

你的数据更新频率是?
    |
    ├─ 几乎不变(如关于页、帮助文档)
    |      → 静态生成,不设置 revalidate
    |      → 有变化时手动重新部署
    |
    ├─ 定期更新(如每小时、每天)
    |      → ISR + 时间基础 revalidate
    |      → export const revalidate = 3600
    |
    ├─ 不定期更新(如用户发布内容)
    |      → 按需 revalidate
    |      → revalidatePath 或 revalidateTag
    |
    └─ 实时更新(如聊天、实时数据)
           → 动态渲染 + cache: 'no-store'
           → 不要缓存

标签命名策略

如果你选择使用 revalidateTag,建议遵循这样的命名规范:

粒度设计

  • 粗粒度(适合批量失效)

    • posts - 所有文章
    • products - 所有产品
    • users - 所有用户
  • 中粒度(按类别/状态)

    • posts:published - 已发布的文章
    • posts:draft - 草稿
    • products:category:electronics - 电子类产品
  • 细粒度(具体资源)

    • post:id:123 - ID为123的文章
    • user:profile:456 - ID为456的用户资料

命名规范建议

使用命名空间,格式为 entity:type:id:

// 获取数据时
const post = await fetch(`/api/posts/${id}`, {
  next: {
    tags: [
      'posts',                    // 粗粒度
      'posts:published',          // 中粒度
      `post:id:${id}`             // 细粒度
    ]
  }
})

// 失效时可以选择不同粒度
revalidateTag('posts')              // 失效所有文章
revalidateTag('posts:published')    // 只失效已发布的
revalidateTag(`post:id:${id}`)      // 只失效特定文章

性能优化建议

1. 不要过度缓存

缓存不是越多越好。缓存太多,会带来这些问题:

  • 数据不一致
  • 调试困难
  • 存储空间浪费

经验法则:

  • 用户个性化数据(如购物车、个人设置)→ 不缓存
  • 公共数据(如商品列表、文章列表)→ 缓存
  • 实时数据(如库存、在线人数)→ 不缓存或极短时间缓存

2. 合理设置 revalidate 时间

不要设置过短的时间。如果你设置 revalidate: 1,意味着每秒都可能重新生成页面,这根本起不到缓存的作用。

推荐设置:

  • 新闻资讯:30-60分钟
  • 博客文章:1-2小时
  • 产品目录:2-4小时
  • 静态页面:24小时或更长

3. 使用 stale-while-revalidate

Next.js 15 的 profile="max" 就是这个策略:

revalidateTag('posts', { profile: 'max' })

用户永远看到的是缓存内容(快!),系统在后台悄悄更新缓存。这是最佳的用户体验。

4. 监控缓存命中率

.env.local 添加:

NEXT_PRIVATE_DEBUG_CACHE=1

然后运行生产服务器,控制台会显示缓存命中情况:

○ GET /blog 200 in 45ms (cache: HIT)
○ GET /about 200 in 12ms (cache: SKIP)

定期检查,看看你的缓存策略是否有效。

开发与生产环境的区别

关键提醒:

开发环境(npm run dev)和生产环境(npm start)的缓存行为完全不同。

特性开发环境生产环境
Data Cache大部分禁用完全启用
Full Route Cache禁用静态路由启用
Request Memoization启用启用
Router Cache启用但时间很短完整时间

测试缓存功能的正确姿势:

# 1. 构建生产版本
npm run build

# 2. 查看终端输出,确认路由类型
#    ○ = 静态,会缓存
#    ƒ = 动态,不会缓存

# 3. 启动生产服务器
npm start

# 4. 测试缓存行为
# 打开浏览器,访问页面,然后修改数据源
# 刷新页面,检查是否还是旧数据

# 5. 测试 revalidate
# 等待 revalidate 时间到期,再次访问,看是否更新

不要在开发环境调试缓存问题。这是最常见的坑,我见过太多人在开发环境折腾半天,然后抱怨 revalidate 不工作。

结论

说了这么多,其实核心就几点:

1. 理解四层缓存的职责

不要把它们混淆。Router Cache 是客户端的,其他三个是服务端的。revalidate 主要影响 Data Cache 和 Full Route Cache。

2. 选择合适的 revalidate 方法

  • 定时更新export const revalidate = 3600
  • 用户触发,单个页面revalidatePath('/page')
  • 用户触发,多个页面revalidateTag('tag')
  • 立即失效updateTag('tag')(Next.js 15)

3. 生产环境测试

永远记住:开发环境的缓存行为不准确。想测试缓存,必须 npm run build && npm start

4. 逐层排查问题

数据不更新时,按这个顺序检查:

  1. Router Cache(客户端)→ 硬刷新试试
  2. Full Route Cache(服务端)→ revalidatePath
  3. Data Cache(服务端)→ revalidateTag
  4. 数据源本身

5. 显式优于隐式(Next.js 15的哲学)

Next.js 15 把缓存从默认启用改成了默认禁用。这意味着你需要主动思考哪些数据需要缓存。虽然麻烦,但长期来看,这让你的代码更清晰,更容易维护。


最后,如果你读到这里,说明你已经对 Next.js 的缓存机制有了完整的理解。下次再遇到数据不更新的问题,你应该知道从哪里下手了。

缓存确实复杂,但搞清楚之后,你会发现它是 Next.js 最强大的特性之一。合理使用缓存,你的应用能快得飞起;胡乱缓存,那就是自己给自己挖坑。

祝你的应用性能爆表,bug为零!

Next.js缓存机制完整使用流程

从理解四层缓存到选择revalidate方法、排查数据不更新问题的完整步骤

⏱️ 预计耗时: 2 小时

  1. 1

    步骤1: 理解Next.js四层缓存

    四层缓存:

    1. Request Memoization(请求去重)
    • 同一请求中,相同URL只发一次请求
    • 自动处理,无需配置
    • 生命周期:单次请求

    2. Data Cache(fetch缓存)
    • fetch请求的缓存
    • 默认行为:Next.js 14缓存,15不缓存
    • 配置:cache选项

    3. Full Route Cache(完整路由缓存)
    • 整个路由的HTML缓存
    • 静态页面自动缓存
    • 配置:revalidate

    4. Router Cache(客户端路由缓存)
    • 客户端导航时的缓存
    • 自动处理,无需配置
    • 生命周期:会话期间

    关键点:Router Cache是客户端的,其他三个是服务端的。
  2. 2

    步骤2: 选择合适的revalidate方法

    三种方法:

    1. 定时更新(export const revalidate)
    ```tsx
    export const revalidate = 3600 // 3600秒后重新验证
    ```
    • 适用:定时更新的内容
    • 配置:在page.tsx或layout.tsx中

    2. 用户触发,单个页面(revalidatePath)
    ```tsx
    import { revalidatePath } from 'next/cache'

    revalidatePath('/blog/post-1')
    ```
    • 适用:用户操作后更新单个页面
    • 使用:在Server Action或API Route中

    3. 用户触发,多个页面(revalidateTag)
    ```tsx
    import { revalidateTag } from 'next/cache'

    // fetch时加tag
    fetch(url, { next: { tags: ['posts'] } })

    // 更新时清除tag
    revalidateTag('posts')
    ```
    • 适用:用户操作后更新多个页面
    • 使用:在Server Action或API Route中

    选择建议:
    • 定时更新 → export const revalidate
    • 单个页面 → revalidatePath
    • 多个页面 → revalidateTag
  3. 3

    步骤3: 排查数据不更新问题

    排查顺序:

    1. 检查是否在开发环境
    • 开发环境缓存行为不准确
    • 必须用生产环境测试:npm run build && npm start

    2. 检查Router Cache(客户端)
    • 硬刷新试试(Ctrl+Shift+R)
    • 清除浏览器缓存

    3. 检查Full Route Cache(服务端)
    • 使用revalidatePath清除
    • 检查revalidate配置

    4. 检查Data Cache(服务端)
    • 使用revalidateTag清除
    • 检查fetch的cache配置

    5. 检查数据源
    • 确认数据源是否真的更新了
    • 检查API返回的数据

    常见问题:
    • 在开发环境测试 → 用生产环境
    • Router Cache没清除 → 硬刷新
    • revalidate配置错误 → 检查配置
    • Next.js 15默认不缓存 → 显式配置cache
  4. 4

    步骤4: Next.js 14 vs 15的缓存差异

    Next.js 14:
    • fetch默认缓存(等同于getStaticProps)
    • 需要显式禁用:cache: 'no-store'

    Next.js 15:
    • fetch默认不缓存
    • 需要显式启用:cache: 'force-cache'

    迁移建议:
    • 检查所有fetch调用
    • 显式配置cache选项
    • 测试缓存行为

    代码示例:
    ```tsx
    // Next.js 14(默认缓存)
    fetch(url) // 自动缓存

    // Next.js 15(默认不缓存)
    fetch(url, { cache: 'force-cache' }) // 需要显式配置
    ```

    关键点:Next.js 15的哲学是"显式优于隐式",需要主动思考哪些数据需要缓存。

常见问题

Next.js有几层缓存?每层管什么?
四层缓存:

1. Request Memoization(请求去重)
• 同一请求中,相同URL只发一次请求
• 自动处理,无需配置
• 生命周期:单次请求

2. Data Cache(fetch缓存)
• fetch请求的缓存
• 默认行为:Next.js 14缓存,15不缓存
• 配置:cache选项

3. Full Route Cache(完整路由缓存)
• 整个路由的HTML缓存
• 静态页面自动缓存
• 配置:revalidate

4. Router Cache(客户端路由缓存)
• 客户端导航时的缓存
• 自动处理,无需配置
• 生命周期:会话期间

关键点:Router Cache是客户端的,其他三个是服务端的。revalidate主要影响Data Cache和Full Route Cache。
revalidatePath、revalidateTag、updateTag有什么区别?
revalidatePath(单个页面):
• 清除指定路径的缓存
• 适用:用户操作后更新单个页面
• 使用:revalidatePath('/blog/post-1')

revalidateTag(多个页面):
• 清除指定tag的所有缓存
• 适用:用户操作后更新多个页面
• 使用:fetch时加tag,更新时清除tag

updateTag(立即失效,Next.js 15):
• 立即标记tag为过期
• 适用:需要立即失效的场景
• 使用:updateTag('tag')

选择建议:
• 单个页面 → revalidatePath
• 多个页面 → revalidateTag
• 立即失效 → updateTag(Next.js 15)

注意:revalidateTag和updateTag需要配合fetch的tags使用。
为什么设置了revalidate数据还是不更新?
可能原因:

1. 在开发环境测试
• 开发环境缓存行为不准确
• 必须用生产环境:npm run build && npm start

2. Router Cache没清除
• 客户端缓存还在
• 硬刷新试试(Ctrl+Shift+R)

3. revalidate配置错误
• 检查revalidate值
• 检查配置位置

4. Next.js 15默认不缓存
• 需要显式配置cache: 'force-cache'
• 检查fetch的cache配置

5. 数据源本身没更新
• 确认数据源是否真的更新了
• 检查API返回的数据

排查顺序:
1. 检查是否在开发环境
2. 检查Router Cache(硬刷新)
3. 检查Full Route Cache(revalidatePath)
4. 检查Data Cache(revalidateTag)
5. 检查数据源
Next.js 14和15的缓存有什么区别?
主要区别:

Next.js 14:
• fetch默认缓存(等同于getStaticProps)
• 需要显式禁用:cache: 'no-store'
• 行为:隐式缓存

Next.js 15:
• fetch默认不缓存
• 需要显式启用:cache: 'force-cache'
• 行为:显式配置

迁移建议:
• 检查所有fetch调用
• 显式配置cache选项
• 测试缓存行为

代码示例:
```tsx
// Next.js 14(默认缓存)
fetch(url) // 自动缓存

// Next.js 15(默认不缓存)
fetch(url, { cache: 'force-cache' }) // 需要显式配置
```

关键点:Next.js 15的哲学是"显式优于隐式",需要主动思考哪些数据需要缓存。这是破坏性变更,迁移时需要注意。
什么时候用revalidatePath,什么时候用revalidateTag?
revalidatePath(单个页面):
• 适用:用户操作后更新单个页面
• 示例:编辑文章后更新文章详情页
• 使用:revalidatePath('/blog/post-1')

revalidateTag(多个页面):
• 适用:用户操作后更新多个页面
• 示例:发布新文章后更新所有列表页
• 使用:fetch时加tag,更新时清除tag

选择建议:
• 只更新一个页面 → revalidatePath
• 更新多个页面 → revalidateTag

代码示例:
```tsx
// 单个页面
revalidatePath('/blog/post-1')

// 多个页面
fetch(url, { next: { tags: ['posts'] } })
revalidateTag('posts') // 清除所有带'posts' tag的缓存
```

关键点:revalidateTag需要配合fetch的tags使用,更灵活。
开发环境和生产环境的缓存行为一样吗?
不一样。开发环境的缓存行为不准确。

开发环境:
• 缓存行为不稳定
• 可能不缓存
• 不适合测试缓存

生产环境:
• 缓存行为准确
• 正常缓存
• 适合测试缓存

测试缓存:
• 必须用生产环境:npm run build && npm start
• 不要用开发环境测试缓存

常见错误:
• 在开发环境测试缓存 → 结果不准确
• 抱怨revalidate不工作 → 实际是环境问题

建议:永远在生产环境测试缓存行为。

20 分钟阅读 · 发布于: 2025年12月19日 · 修改于: 2026年1月22日

评论

使用 GitHub 账号登录后即可评论

相关文章