Next.js 中的缓存机制
Next.js 通过缓存渲染工作和数据请求来提升应用程序的性能并降低成本。本页深入介绍了 Next.js 的缓存机制、你可以用来配置它们的 API,以及它们之间的交互方式。
提示:本页帮助你了解 Next.js 的内部工作原理,但不是使用 Next.js 进行高效开发的必备知识。Next.js 的大部分 缓存启发式算法由你的 API 使用情况决定,并为最佳性能提供了零配置或最小配置的默认设置。如果你想直接查看示例,请从这里开始。
概述
以下是不同缓存机制及其用途的高级概述:
| 机制 | 缓存内容 | 位置 | 目的 | 持续时间 |
|---|---|---|---|---|
| 请求记忆化 | 函数的返回值 | 服务端 | 在 React 组件树中重用数据 | 单次请求生命周期 |
| 数据缓存 | 数据 | 服务端 | 跨用户请求和部署存储数据 | 持久化(可重新验证) |
| 完整路由缓存 | HTML 和 RSC 载荷 | 服务端 | 减少渲染成本并提升性能 | 持久化(可重新验证) |
| 路由缓存 | RSC 载荷 | 客户端 | 减少导航时的服务端请求 | 用户会话或基于时间 |
默认情况下,Next.js 会尽可能多地缓存内容以提升性能并降低成本。这意味着路由会被静态渲染,数据请求会被缓存,除非你选择退出。下图显示了默认的缓存行为:当路由在构建时被静态渲染,以及当静态路由首次被访问时。


缓存行为会根据路由是静态还是动态渲染、数据是否被缓存、以及请求是初始访问还是后续导航而变化。根据你的使用场景,你可以为单个路由和数据请求配置缓存行为。
在 middleware 中不支持 fetch 缓存。在 middleware 内部进行的任何 fetch 请求都不会被缓存。
请求记忆化
Next.js 扩展了 fetch API,自动记忆化具有相同 URL 和选项的请求。这意味着你可以在 React 组件树的多个位置调用同一个 fetch 函数来获取相同的数据,而只会执行一次。


例如,如果你需 要在路由中使用相同的数据(例如在 Layout、Page 和多个组件中),你不必在树的顶部获取数据,并在组件之间传递 props。相反,你可以在需要数据的组件中获取数据,而不必担心为相同数据在网络中发出多个请求的性能影响。
- TypeScript
- JavaScript
async function getItem() {
// `fetch` 函数会自动记忆化,结果会被缓存
const res = await fetch('https://.../item/1')
return res.json()
}
// 这个函数被调用了两次,但只在第一次执行
const item = await getItem() // 缓存 MISS
// 第二次调用可能在你路由的任何地方
const item = await getItem() // 缓存 HIT
async function getItem() {
// `fetch` 函数会自动记忆化,结果会被缓存
const res = await fetch('https://.../item/1')
return res.json()
}
// 这个函数被调用了两次,但只在第一次执行
const item = await getItem() // 缓存 MISS
// 第二次调用可能在你路由的任何地方
const item = await getItem() // 缓存 HIT
请求记忆化的工作原理


- 在渲染路由时,第一次调用特定请求时,其结果不会在内存中,这将是一个缓存
MISS。 - 因此,函数将被执行,数据将从外部源获取,结果将存储在内存中。
- 在同一渲染过程中对请求的后续函数调用将是缓存
HIT,数据将从内存返回而不执行函数。 - 一旦路由渲染完成且渲染过程结束,内存会被"重置",所有请求记忆化条目都会被清除。
提示:
- 请求记忆化是 React 的功能,不是 Next.js 的功能。这里包含它是为了展示它如何与其他缓存机制交互。
- 记忆化仅适用于
fetch请求中的GET方法。- 记忆化仅适用于 React 组件树,这意味着:
- 它适用于
generateMetadata、generateStaticParams、Layouts、Pages 和其他服务端组件中的fetch请求。- 它不适用于路由处理器中的
fetch请求,因为它们不是 React 组件树的一部分。- 对于
fetch不适合的情况(例如某些数据库客户端、CMS 客户端或 GraphQL 客户端),你可以使用 Reactcache函数 来记忆化函数。
持续时间
缓存在服务端请求的生命周期内持续,直到 React 组件树完成渲染。
重新验证
由于记忆化不在服务端请求之间共享,仅适用于渲染期间,因此无需重新验证。
选择退出
记忆化仅适用于 fetch 请求中的 GET 方法,其他方法(如 POST 和 DELETE)不会被记忆化。这种默认行为是 React 的优化,我们不建议退出。
要管理单个请求,你可以使用 AbortController 的 signal 属性。
const { signal } = new AbortController()
fetch(url, { signal })
数据缓存
Next.js 有一个内置的数据缓存,可以持久化跨传入服务端请求和部署的数据获取结果。这是因为 Next.js 扩展了原生 fetch API,允许服务端的每个请求设置自己的持久化缓存语义。
提示:在浏览器中,
fetch的cache选项表示请求如何与浏览器的 HTTP 缓存交互,在 Next.js 中,cache选项表示服务端请求如何与服务端的数据缓存交互。
你可以使用 fetch 的 cache 和 next.revalidate 选项来配置缓存行为。
在开发模式下,fetch 数据会为热模块替换(HMR)重用,缓存选项对于硬刷新会被忽略。
数据缓存的工作原理


- 在渲染期间第一次调用带有
'force-cache'选项的fetch请求时,Next.js 会检查数据缓存中是否有缓存的响应。 - 如果找到缓存的响应,它会立即返回并被记忆化。
- 如果没有找到缓存的响应,请求会发送到数据源,结果会存储在数据缓存中并被记忆化。
- 对于未缓存的数据(例如未定义
cache选项或使用{ cache: 'no-store' }),结果总是从数据源获取并被记忆化。 - 无论数据是否被缓存,请求总是被记忆化以避免在 React 渲染过程中为相同数据发出重复请求。
数据缓存和请求记忆化之间的区别
虽然两种缓存机制都通过重用缓存数据来帮助提升性能,但数据缓存在传入请求和部署之间是持久化的,而记忆化仅在请求的生命周期内持续。
持续时间
数据缓存在传入请求和部署之间是持久化的,除非你重新验证或选择退出。
重新验证
缓存数据可以通过两种方式重新验证:
- 基于时间的重新验证:在特定时间过去后重新验证数据,并发出新请求。这对于不经常变化且新鲜度不是那么关键的数据很有用。
- 按需重新验证:基于事件重新验证数据(例如表单提交)。按需重新验证可以使用基于标签或基于路径的方法来一次性重新验证数据组。当你希望确保尽快显示最新数据时(例如当你的无头 CMS 的内容更新时),这很有用。
基于时间的重新验证
要以定时间隔重新验证数据,你可以使用 fetch 的 next.revalidate 选项来设置资源的缓存生命周期(以秒为单位)。
// 最多每小时重新验证一次
fetch('https://...', { next: { revalidate: 3600 } })
或者,你可以使用路由段配置选项来配置段中的所有 fetch 请求,或者用于无法使用 fetch 的情况。
基于时间的重新验证的工作原理


- 第一次调用带有
revalidate的 fetch 请求时,数据将从外部数据源获取并存储在数据缓存中。 - 在指定时间范围内(例如 60 秒)调用的任何请求将返回缓存的数据。
- 时间范围过后,下一个请求仍将返回缓存(现在是过时的)数据。
- Next.js 将在后台触发数据的重新验证。
- 一旦数据成功获取,Next.js 将用新鲜数据更新数据缓存。
- 如果后台重新验证失败,将保持之前的数据不变。
这类似于stale-while-revalidate 行为。
按需重新验证
数据可以通过路径(revalidatePath)或缓存标签(revalidateTag)按需重新验证。
按需重新验证的工作原理


- 第一次调用
fetch请求时,数据将从外部数据源获取并存储在数据缓存中。 - 当触发按需重新验证时,相应的缓存条目将从缓存中清除。
- 这与基于时间的重新验证不同,后者在获取新鲜数据之前会在缓存中保持过时数据。
- 下次发出请求时,它将再次成为缓存
MISS,数据将从外部数据源获取并存储在数据缓存中。
选择退出
如果你不想缓存 fetch 的响应,可以执行以下操作:
let data = await fetch('https://api.vercel.app/blog', { cache: 'no-store' })
完整路由缓存
相关术语:
你可能会看到自动静态优化、静态站点生成或静态渲染这些术语被互换使用,指的是在构建时渲染和缓存应用程序路由的过程。
Next.js 在构建时自动渲染和缓存路 由。这是一种优化,允许你提供缓存的路由,而不是为每个请求在服务端渲染,从而实现更快的页面加载。
要理解完整路由缓存的工作原理,了解 React 如何处理渲染以及 Next.js 如何缓存结果是有帮助的:
1. 服务端的 React 渲染
在服务端,Next.js 使用 React 的 API 来编排渲染。渲染工作被分割成块:按单个路由段和 Suspense 边界。
每个块分两步渲染:
- React 将服务端组件渲染成一种特殊的数据格式,针对流式传输进行了优化,称为React 服务端组件载荷。
- Next.js 使用 React 服务端组件载荷和客户端组件 JavaScript 指令在服务端渲染 HTML。
这意味着我们不必等待所有内容渲染完成就可以缓存工作或发送响应。相反,我们可以在工作完成时流式传输响应。
什么是 React 服务端组件载荷?
React 服务端组件载荷是渲染的 React 服务端组件树的紧凑二进制表示。React 在客户端使用它来更新浏览器的 DOM。React 服务端组件载荷包含:
- 服务端组件的渲染结果
- 客户端组件应该渲染的位置的占位符以及对其 JavaScript 文件的引用
- 从服务端组件传递给客户端组件的任何 props
要了解更多信息,请参阅服务端组件文档。
2. Next.js 服务端缓存(完整路由缓存)


Next.js 的默认行为是在服务端缓存路由的渲染结果(React 服务端组件载荷和 HTML)。这适用于在构建时静态渲染的路由,或在重新验证期间。
3. 客户端的 React 水合和协调
在请求时,在客户端:
- HTML 用于立即显示客户端和服务端组件的快速非交互式初始预览。
- React 服务端组件载荷用于协调客户端和渲染的服务端组件树,并更新 DOM。
- JavaScript 指令用于水合客户端组件并使应用程序具有交互性。
4. Next.js 客户端缓存(路由缓存)
React 服务端组件载荷存储在客户端路由缓存中 - 一个单独的进程内缓存,按单个路由段分割。这个路由缓存用于通过存储之前访问的路由和预取未来路由来改善导航体验。
5. 后续导航
在后续导航或预取期间,Next.js 将检查 React 服务端组件载荷是否存储在路由缓存中。如果是,它将跳过向服务端发送新请求。
如果路由段不在缓存中,Next.js 将从服务端获取 React 服务端组件载荷,并在客户端填充路由缓存。
静态和动态渲染
路由是否在构建时被缓存取决于它是静态还是动态渲染。静态路由默认被缓存,而动态路由在请求时渲染,不被缓存。
此图显示了静态和动态渲染路由之间的区别,包括缓存和未缓存的数据:


了解更多关于静态和动态渲染的信息。
持续时间
默认情况下,完整路由缓存是持久化的。这意味着渲染输出在用户请求之间被缓存。
失效
你可以通过两种方式使完整路由缓存失效:
选择退出
你可以通过以下方式选择退出完整路由缓存,换句话说,为每个传入请求动态渲染组件:
- 使用动态 API:这将使路由退出完整路由缓存并在请求时动态渲染。数据缓存仍可使用。
- 使用
dynamic = 'force-dynamic'或revalidate = 0路由段配置选项:这将跳过完整路由缓存和数据缓存。这意味着组件将在每个传入请求到服务端时被渲染,数据将被获取。路由缓存仍将适用,因为它是客户端缓存。 - 选择退出数据缓存:如果路由有一个未缓存的
fetch请求,这将使路由退出完整路由缓存。特定fetch请求的数据将在每个传入请求时被获取。明确启用缓存的其他fetch请求仍将在数据缓存中被缓存。这允许缓存和未缓存数据的混合。
客户端路由缓存
Next.js 有一个进程内客户端路由缓存,存储路由段的 RSC 载荷,按布局、加载状态和页面分割。
当用户在路由之间导航时,Next.js 缓存访问的路由段并预取用户可能导航到的路由。这实现了即时前进/后退导航、导航之间无完整页面重新加载,以及共享布局中浏览器状态和 React 状态的保持。
通过路由缓存:
- 布局在导航时被缓存和重用(部分渲染)。
- 加载状态在导航时被缓存和重用,用于即时导航。
- 页面默认不被缓存,但在浏览器前进和后退导航期间被重用。你可以通过使用实验性的
staleTimes配置选项为页面段启用缓存。
提示:此缓存专门适用于 Next.js 和服务端组件,与浏览器的 bfcache 不同,尽管它有类似的结果。
持续时间
缓存存储在浏览器的临时内存中。两个因素决定路由缓存持续多长时间:
- 会话:缓存在导航之间持续。但是,它会在页面刷新时被清除。
- 自动失效期:布局和加载状态的缓存在特定时间后自动失效。持续时间取决于资源如何被预取,以及资源是否被静态生成:
- 默认预取(
prefetch={null}或未指定):动态页面不缓存,静态页面 5 分钟。 - 完全预取(
prefetch={true}或router.prefetch):静态和动态页面都是 5 分钟。
- 默认预取(
虽然页面刷新会清除所有缓存的段,但自动失效期只影响从预取时间开始的单个段。
提示:实验性的
staleTimes配置选项可用于调整上述自动失效时间。
失效
你可以通过两种方式使路由缓存失效:
- 在服务端操作中:
- 通过路径按需重新验证数据(
revalidatePath)或通过缓存标签(revalidateTag) - 使用
cookies.set或cookies.delete使路由缓存失效,以防止使用 cookie 的路由变得过时(例如身份验证)。
- 通过路径按需重新验证数据(
- 调用
router.refresh将使路由缓存失效并为当前路由向服务端发出新请求。
选择退出
从 Next.js 15 开始,页面段默认选择退出。
提示:你还可以通过将
<Link>组件的prefetch属性设置为false来选择退出预取。
缓存交互
在配置不同的缓存机制时,了解它们如何相互交互很重要:
数据缓存和完整路由缓存
- 重新验证或选择退出数据缓存将使完整路由缓存失效,因为渲染输出依赖于数据。
- 使完整路由缓存失效或选择退出不会影响数据缓存。你可以动态渲染一个同时具有缓存和未缓存数据的路由。当你的页面大部分使用缓存数据,但你有几个依赖于需要在请求时获取的数据的组件时,这很有用。你可以动态渲染而不必担心重新获取所有数据的性能影响。
数据缓存和客户端路由缓存
- 要立即使数据缓存和路由缓存失效,你可以在服务端操作中使用
revalidatePath或revalidateTag。 - 在路由处理器中重新验证数据缓存不会立即使路由缓存失效,因为路由处理器不绑定到特定路由。这意味着路由缓存将继续提供之前的载荷,直到硬刷新或自动失效期结束。
API
下表概述了不同的 Next.js API 如何影响缓存:
| API | 路由缓存 | 完整路由缓存 | 数据缓存 | React 缓存 |
|---|---|---|---|---|
<Link prefetch> | 缓存 | |||
router.prefetch | 缓存 | |||
router.refresh | 重新验证 | |||
fetch | 缓存 | 缓存(GET 和 HEAD) | ||
fetch options.cache | 缓存或选择退出 | |||
fetch options.next.revalidate | 重新验证 | 重新验证 | ||
fetch options.next.tags | 缓存 | 缓存 | ||
revalidateTag | 重新验证(服务端操作) | 重新验证 | 重新验证 | |
revalidatePath | 重新验证(服务端操作) | 重新验证 | 重新验证 | |
const revalidate | 重新验证或选择退出 | 重新验证或选择退出 | ||
const dynamic | 缓存或选择退出 | 缓存或选择退出 | ||
cookies | 重新验证(服务端操作) | 选择退出 | ||
headers, searchParams | 选择退出 | |||
generateStaticParams | 缓存 | |||
React.cache | 缓存 | |||
unstable_cache | 缓存 |
<Link>
默认情况下,<Link> 组件自动从完整路由缓存预取路由,并将 React 服务端组件载荷添加到路由缓存。
要禁用预取,你可以将 prefetch 属性设置为 false。但这不会永久跳过缓存,当用户访问路由时,路由段仍会在客户端被缓存。
了解更多关于 <Link> 组件的信息。
router.prefetch
useRouter hook 的 prefetch 选项可用于手动预取路由。这会将 React 服务端组件载荷添加到路由缓存。
请参阅 useRouter hook API 参考。
router.refresh
useRouter hook 的 refresh 选项可用于手动刷新路由。这会完全清除路由缓存,并为当前路由向服务端发出新请求。refresh 不会影响数据或完整路由缓存。
渲染结果将在客户端协调,同时保持 React 状态和浏览器状态。
请参阅 useRouter hook API 参考。
fetch
从 fetch 返回的数据在数据缓存中不会自动缓存。
fetch 的默认缓存行为(例如,当未指定 cache 选项时)等于将 cache 选项设置为 no-store:
let data = await fetch('https://api.vercel.app/blog', { cache: 'no-store' })
请参阅 fetch API 参考