Next.js 中的缓存机制

Next.js 通过缓存渲染工作和数据请求来提升应用性能并降低成本。本文深入探讨 Next.js 的缓存机制、可配置的 API 以及它们之间的交互关系。

须知:本文帮助您理解 Next.js 底层工作原理,但并非高效使用 Next.js 的必备知识。大多数 Next.js 缓存启发式规则由您的 API 使用情况决定,并已预设为最佳性能,通常无需或只需少量配置。

概览

以下是不同缓存机制及其用途的高级概览:

机制缓存内容位置用途持续时间
请求记忆化函数返回值服务端在 React 组件树中复用数据单次请求生命周期
数据缓存数据服务端跨用户请求和部署存储数据持久性(可重新验证)
全路由缓存HTML 和 RSC 负载服务端降低渲染成本并提升性能持久性(可重新验证)
路由缓存RSC 负载客户端减少导航时的服务端请求用户会话或基于时间

默认情况下,Next.js 会尽可能缓存以提升性能并降低成本。这意味着路由会静态渲染且数据请求会被缓存,除非您主动退出。下图展示了默认缓存行为:当路由在构建时静态渲染,以及当静态路由首次被访问时。

展示 Next.js 中四种机制默认缓存行为的示意图,包含构建时和路由首次访问时的 HIT、MISS 和 SET 状态

缓存行为会根据路由是静态还是动态渲染、数据是否缓存、请求是首次访问还是后续导航而变化。根据您的用例,可以为单个路由和数据请求配置缓存行为。

请求记忆化

React 扩展了 fetch API,自动记忆化具有相同 URL 和选项的请求。这意味着您可以在 React 组件树的多个位置调用相同的 fetch 函数,而实际只会执行一次。

去重化的 Fetch 请求

例如,如果您需要在路由中多处使用相同数据(如在布局、页面和多个组件中),您无需在树顶部获取数据并通过组件传递 props。相反,您可以在需要数据的组件中直接获取数据,而无需担心因网络重复请求相同数据带来的性能影响。

async function getItem() {
  // `fetch` 函数会自动记忆化,结果会被缓存
  const res = await fetch('https://.../item/1')
  return res.json()
}

// 此函数被调用两次,但仅第一次会执行
const item = await getItem() // 缓存 MISS

// 第二次调用可以发生在路由的任何位置
const item = await getItem() // 缓存 HIT

请求记忆化工作原理

展示 React 渲染期间 fetch 记忆化工作原理的示意图
  • 在渲染路由时,首次调用特定请求时,其结果不在内存中,会触发缓存 MISS
  • 因此,函数将被执行,数据会从外部源获取,结果会存储在内存中。
  • 同一渲染过程中对相同请求的后续函数调用会触发缓存 HIT,数据会直接从内存返回而无需执行函数。
  • 路由渲染完成且渲染过程结束后,内存会被"重置",所有请求记忆化条目都会被清除。

须知

  • 请求记忆化是 React 特性,而非 Next.js 特性。此处提及是为了展示其与其他缓存机制的交互。
  • 记忆化仅适用于 fetch 请求的 GET 方法。
  • 记忆化仅适用于 React 组件树,这意味着:
    • 它适用于 generateMetadatagenerateStaticParams、布局、页面和其他服务端组件中的 fetch 请求。
    • 不适用于路由处理器中的 fetch 请求,因为它们不属于 React 组件树。
  • 对于不适合使用 fetch 的情况(如某些数据库客户端、CMS 客户端或 GraphQL 客户端),可以使用 React cache 函数 来记忆化函数。

持续时间

缓存持续存在于服务端请求的整个生命周期,直到 React 组件树完成渲染。

重新验证

由于记忆化不跨服务端请求共享且仅在渲染期间有效,因此无需重新验证。

退出机制

记忆化仅适用于 fetch 请求的 GET 方法,其他方法如 POSTDELETE 不会被记忆化。此默认行为是 React 的优化措施,我们不建议退出。

要管理单个请求,可以使用 AbortControllersignal 属性。但这不会使请求退出记忆化,而是中止进行中的请求。

app/example.js
const { signal } = new AbortController()
fetch(url, { signal })

数据缓存

Next.js 内置了数据缓存,可在传入的服务端请求部署间持久化数据获取结果。这是通过扩展原生 fetch API 实现的,允许每个服务端请求设置自己的持久化缓存语义。

须知:在浏览器中,fetchcache 选项表示请求如何与浏览器的 HTTP 缓存交互;在 Next.js 中,cache 选项表示服务端请求如何与服务端的数据缓存交互。

默认情况下,使用 fetch 的数据请求会被缓存。您可以使用 fetchcachenext.revalidate 选项来配置缓存行为。

数据缓存工作原理

展示缓存和非缓存 fetch 请求如何与数据缓存交互的示意图。缓存请求存储在数据缓存中并被记忆化,非缓存请求从数据源获取,不存储在数据缓存中,但会被记忆化。
  • 在渲染期间首次调用 fetch 请求时,Next.js 会检查数据缓存中是否有缓存响应。
  • 如果找到缓存响应,则立即返回并记忆化
  • 如果未找到缓存响应,则向数据源发起请求,结果存储在数据缓存中并被记忆化。
  • 对于非缓存数据(如 { cache: 'no-store' }),结果总是从数据源获取并被记忆化。
  • 无论数据是否缓存,请求总是被记忆化以避免在 React 渲染过程中对相同数据发起重复请求。

数据缓存与请求记忆化的区别

虽然两种缓存机制都通过复用缓存数据提升性能,但数据缓存在传入请求和部署间持久化,而记忆化仅持续单次请求的生命周期。

通过记忆化,我们减少了同一渲染过程中必须跨越网络边界(从渲染服务器到数据缓存服务器,如 CDN 或边缘网络)或数据源(如数据库或 CMS)的重复请求数量。通过数据缓存,我们减少了对原始数据源的请求数量。

持续时间

数据缓存在传入请求和部署间持久化,除非您重新验证或退出。

重新验证

缓存数据可通过两种方式重新验证:

  • 基于时间的重新验证:在一定时间间隔后重新验证数据。适用于变化不频繁且实时性要求不高的数据。
  • 按需重新验证:基于事件(如表单提交)重新验证数据。按需重新验证可使用基于标签或路径的方法一次性重新验证一组数据。适用于需要尽快显示最新数据的场景(如无头 CMS 内容更新时)。

基于时间的重新验证

要按时间间隔重新验证数据,可使用 fetchnext.revalidate 选项设置资源的缓存生命周期(秒)。

// 最多每小时重新验证一次
fetch('https://...', { next: { revalidate: 3600 } })

或者,可以使用路由段配置选项配置段中所有 fetch 请求,或用于无法使用 fetch 的情况。

基于时间的重新验证工作原理

展示基于时间重新验证工作原理的示意图,重新验证周期后,首次请求返回陈旧数据,随后数据被重新验证。
  • 首次调用带 revalidate 的 fetch 请求时,数据会从外部数据源获取并存储在数据缓存中。
  • 在指定时间范围内(如 60 秒)的任何请求都会返回缓存数据。
  • 时间范围过后,下一次请求仍会返回缓存(现已陈旧)数据。
    • Next.js 会在后台触发数据重新验证。
    • 数据成功获取后,Next.js 会用新数据更新数据缓存。
    • 如果后台重新验证失败,则保留原有数据不变。

这与 stale-while-revalidate 行为类似。

按需重新验证

数据可通过路径 (revalidatePath) 或缓存标签 (revalidateTag) 按需重新验证。

按需重新验证工作原理

展示按需重新验证工作原理的示意图,重新验证请求后数据缓存更新为新数据。
  • 首次调用 fetch 请求时,数据会从外部数据源获取并存储在数据缓存中。
  • 触发按需重新验证时,相应的缓存条目会从缓存中清除。
    • 这与基于时间的重新验证不同,后者在获取新数据前会保留陈旧数据。
  • 下次发起请求时,会再次触发缓存 MISS,数据会从外部数据源获取并存储在数据缓存中。

退出机制

对于单个数据获取,可通过将 cache 选项设为 no-store 来退出缓存。这意味着每次调用 fetch 都会获取数据。

// 为单个 `fetch` 请求退出缓存
fetch(`https://...`, { cache: 'no-store' })

或者,也可以使用路由段配置选项为特定路由段退出缓存。这将影响路由段中的所有数据请求,包括第三方库。

// 为路由段中所有数据请求退出缓存
export const dynamic = 'force-dynamic'

注意:数据缓存目前仅适用于页面/路由,不适用于中间件。中间件中的任何 fetch 默认都不会被缓存。

Vercel 数据缓存

如果您的 Next.js 应用部署在 Vercel 上,建议阅读 Vercel 数据缓存 文档以了解 Vercel 特定功能。

全路由缓存

相关术语

您可能会看到自动静态优化静态站点生成静态渲染这些术语互换使用,指在构建时渲染和缓存应用路由的过程。

Next.js 会在构建时自动渲染和缓存路由。这是一项优化,允许您直接提供缓存路由,而无需为每个请求在服务端渲染,从而实现更快的页面加载。

要理解全路由缓存的工作原理,了解 React 如何处理渲染以及 Next.js 如何缓存结果很有帮助:

1. 服务端 React 渲染

在服务端,Next.js 使用 React API 协调渲染。渲染工作被拆分为块:按单个路由段和 Suspense 边界划分。

每个块的渲染分为两步:

  1. React 将服务端组件渲染为一种特殊的流式数据格式,称为 React 服务端组件负载
  2. Next.js 使用 React 服务端组件负载和客户端组件 JavaScript 指令在服务端渲染 HTML

这意味着我们无需等待所有内容渲染完成即可缓存工作或发送响应。相反,我们可以在工作完成时流式传输响应。

什么是 React 服务端组件负载?

React 服务端组件负载是渲染后的 React 服务端组件树的紧凑二进制表示。React 在客户端使用它来更新浏览器的 DOM。React 服务端组件负载包含:

  • 服务端组件的渲染结果
  • 客户端组件应渲染位置的占位符及其 JavaScript 文件引用
  • 从服务端组件传递给客户端组件的任何 props

了解更多,请参阅服务端组件文档。

2. Next.js 服务端缓存(全路由缓存)

全路由缓存的默认行为,展示静态渲染路由的 React 服务端组件负载和 HTML 如何在服务端缓存。

Next.js 的默认行为是在服务端缓存路由的渲染结果(React 服务端组件负载和 HTML)。这适用于构建时静态渲染的路由,或在重新验证期间。

3. 客户端 React 水合与协调

在请求时,客户端会执行以下操作:

  1. 使用 HTML 立即显示客户端和服务器组件的快速非交互式初始预览
  2. 使用 React 服务器组件负载 (React Server Components Payload) 协调客户端和已渲染的服务器组件树,并更新 DOM
  3. 使用 JavaScript 指令对客户端组件进行水合 (hydrate),使应用具备交互性

4. Next.js 客户端缓存(路由缓存)

React 服务器组件负载会被存储在客户端的路由缓存 (Router Cache) 中——这是一个按独立路由段分割的内存缓存。路由缓存通过存储先前访问过的路由和预取未来路由来提升导航体验。

5. 后续导航

在后续导航或预取时,Next.js 会检查 React 服务器组件负载是否已存储在路由缓存中。如果存在,则会跳过向服务器发送新请求。

如果路由段不在缓存中,Next.js 会从服务器获取 React 服务器组件负载,并在客户端填充路由缓存。

静态与动态渲染

路由是否在构建时被缓存取决于它是静态渲染还是动态渲染。静态路由默认会被缓存,而动态路由则在请求时渲染且不被缓存。

下图展示了静态和动态渲染路由的区别,以及缓存与非缓存数据的情况:

静态和动态渲染对完整路由缓存的影响。静态路由在构建时或数据重新验证后被缓存,而动态路由从不被缓存

了解更多关于静态与动态渲染的内容。

持续时间

默认情况下,完整路由缓存 (Full Route Cache) 是持久化的。这意味着渲染输出会在用户请求间被缓存。

失效

有两种方式可以使完整路由缓存失效:

退出缓存

你可以通过以下方式退出完整路由缓存(即对每个传入请求动态渲染组件):

  • 使用动态函数 (Dynamic Function):这将使路由退出完整路由缓存,并在请求时动态渲染。数据缓存仍可使用。
  • 使用路由段配置选项 dynamic = 'force-dynamic'revalidate = 0:这会跳过完整路由缓存和数据缓存,即组件会在每次传入请求时重新渲染,数据也会重新获取。路由缓存仍会生效,因为它是客户端缓存。
  • 退出数据缓存 (Data Cache):如果路由中存在未被缓存的 fetch 请求,则该路由会退出完整路由缓存。该 fetch 请求的数据会在每次传入请求时重新获取。其他未退出缓存的 fetch 请求仍会被缓存在数据缓存中,从而实现缓存与非缓存数据的混合使用。

路由缓存

相关术语:

路由缓存可能被称为客户端缓存 (Client-side Cache)预取缓存 (Prefetch Cache)。其中预取缓存特指预取的路由段,而客户端缓存指整个路由缓存,包括已访问和预取的路由段。 此缓存专用于 Next.js 和服务器组件,与浏览器的 bfcache 不同,尽管效果相似。

Next.js 有一个内存中的客户端缓存,用于在用户会话期间存储按独立路由段分割的 React 服务器组件负载。这被称为路由缓存 (Router Cache)。

路由缓存的工作原理

路由缓存在静态和动态路由中的工作方式,展示初始导航和后续导航的 MISS 和 HIT 情况。

当用户在路由间导航时,Next.js 会缓存已访问的路由段,并根据视口中的 <Link> 组件预取 (prefetch) 用户可能导航到的路由。

这会为用户带来更好的导航体验:

  • 即时前进/后退导航,因为已访问的路由被缓存;快速导航到新路由,因为预取和部分渲染 (partial rendering)
  • 导航间无需整页刷新,React 状态和浏览器状态会被保留。

路由缓存与完整路由缓存的区别

路由缓存在浏览器中临时存储 React 服务器组件负载(仅限用户会话期间),而完整路由缓存在服务器上持久存储 React 服务器组件负载和 HTML(跨多个用户请求)。

完整路由缓存仅缓存静态渲染的路由,而路由缓存适用于静态和动态渲染的路由。

持续时间

缓存存储在浏览器的临时内存中。以下两个因素决定路由缓存的持续时间:

  • 会话 (Session):缓存在导航间持续存在,但在页面刷新时会被清除。
  • 自动失效周期 (Automatic Invalidation Period):布局和加载状态的缓存会在特定时间后自动失效。持续时间取决于资源的预取方式以及资源是否静态生成
    • 默认预取 (Default Prefetching)prefetch={null} 或未指定):动态页面不缓存,静态页面缓存 5 分钟。
    • 完整预取 (Full Prefetching)prefetch={true}router.prefetch):静态和动态页面均缓存 5 分钟。

页面刷新会清除所有缓存的路由段,而自动失效周期仅影响从预取时刻起的单个路由段。

须知:实验性的 staleTimes 配置选项可用于调整上述自动失效时间。

失效

有两种方式可以使路由缓存失效:

  • 服务器操作 (Server Action) 中:
  • 调用 router.refresh 会使路由缓存失效,并为当前路由向服务器发起新请求。

退出缓存

无法完全退出路由缓存,但可以通过调用 router.refreshrevalidatePathrevalidateTag(见上文)使其失效。这将清除缓存并向服务器发起新请求,确保显示最新数据。

也可以通过将 <Link> 组件的 prefetch 属性设为 false 来退出预取。但这仍会临时存储路由段 30 秒,以实现嵌套段(如标签栏)间的即时导航或前进/后退导航。已访问的路由仍会被缓存。

缓存交互

在配置不同的缓存机制时,理解它们如何相互影响非常重要:

数据缓存与完整路由缓存

  • 重新验证或退出数据缓存使完整路由缓存失效,因为渲染输出依赖于数据。
  • 使完整路由缓存失效或退出完整路由缓存不会影响数据缓存。你可以动态渲染一个同时包含缓存和非缓存数据的路由。这在页面大部分使用缓存数据,但少数组件依赖需实时获取的数据时非常有用。你可以动态渲染而无需担心重新获取所有数据对性能的影响。

数据缓存与客户端路由缓存

API

下表概述了不同 Next.js API 对缓存的影响:

API路由缓存完整路由缓存数据缓存React 缓存
<Link prefetch>缓存
router.prefetch缓存
router.refresh重新验证
fetch缓存缓存
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> 组件会自动从完整路由缓存预取路由,并将 React 服务器组件负载添加到路由缓存中。

要禁用预取,可以将 prefetch 属性设为 false。但这不会永久跳过缓存,用户访问路由时仍会在客户端缓存该路由段。

了解更多关于 <Link> 组件的内容。

router.prefetch

useRouter 钩子的 prefetch 选项可用于手动预取路由。这会将 React 服务器组件负载添加到路由缓存中。

参见 useRouter 钩子的 API 参考。

router.refresh

useRouter 钩子的 refresh 选项可用于手动刷新路由。这会完全清除路由缓存,并为当前路由向服务器发起新请求。refresh 不会影响数据缓存或完整路由缓存。

渲染结果会在客户端进行协调,同时保留 React 状态和浏览器状态。

参见 useRouter 钩子的 API 参考。

fetch

fetch 返回的数据会自动缓存在数据缓存中。

// 默认缓存。`force-cache` 是默认选项,可省略。
fetch(`https://...`, { cache: 'force-cache' })

更多选项参见 fetch API 参考

fetch options.cache

可以通过将 cache 选项设为 no-store 来退出单个 fetch 请求的数据缓存:

// 退出缓存
fetch(`https://...`, { cache: 'no-store' })

由于渲染输出依赖于数据,使用 cache: 'no-store' 还会跳过使用该 fetch 请求的路由的完整路由缓存。即该路由会在每次请求时动态渲染,但同一路由中的其他数据请求仍可被缓存。

更多选项参见 fetch API 参考

fetch options.next.revalidate

可以使用 fetchnext.revalidate 选项设置单个 fetch 请求的重新验证周期(秒)。这会重新验证数据缓存,进而重新验证完整路由缓存。新数据会被获取,组件会在服务器上重新渲染。

// 最多 1 小时后重新验证
fetch(`https://...`, { next: { revalidate: 3600 } })

更多选项参见 fetch API 参考

fetch options.next.tagsrevalidateTag

Next.js 提供了一套缓存标签系统,用于实现细粒度的数据缓存与重新验证。

  1. 使用 fetchunstable_cache 时,您可以选择为一个或多个标签标记缓存条目。
  2. 随后,您可以调用 revalidateTag 来清除与该标签关联的缓存条目。

例如,您可以在获取数据时设置标签:

// 使用标签缓存数据
fetch(`https://...`, { next: { tags: ['a', 'b', 'c'] } })

然后,调用 revalidateTag 并指定标签以清除缓存条目:

// 重新验证带有特定标签的条目
revalidateTag('a')

根据您的需求,可以在以下两种场景中使用 revalidateTag

  1. 路由处理器 (Route Handlers) - 用于响应第三方事件(如 Webhook)时重新验证数据。由于路由处理器不与特定路由绑定,这不会立即使路由缓存失效。
  2. 服务器操作 (Server Actions) - 在用户操作(如表单提交)后重新验证数据。这将使关联路由的路由缓存失效。

revalidatePath

revalidatePath 允许您手动重新验证数据 在单次操作中重新渲染特定路径下的路由段。调用 revalidatePath 方法会重新验证数据缓存 (Data Cache),进而使完整路由缓存 (Full Route Cache) 失效。

revalidatePath('/')

根据您的需求,可以在以下两种场景中使用 revalidatePath

  1. 路由处理器 (Route Handlers) - 用于响应第三方事件(如 Webhook)时重新验证数据。
  2. 服务器操作 (Server Actions) - 在用户交互(如表单提交、点击按钮)后重新验证数据。

更多信息请参阅 revalidatePath API 参考文档

revalidatePathrouter.refresh 的区别:

调用 router.refresh 会清除路由缓存 (Router Cache),并在服务器上重新渲染路由段,但不会使数据缓存 (Data Cache) 或完整路由缓存 (Full Route Cache) 失效。

两者的区别在于:revalidatePath 会清除数据缓存和完整路由缓存,而 router.refresh() 作为客户端 API 不会改变数据缓存和完整路由缓存。

动态函数 (Dynamic Functions)

诸如 cookiesheaders 等动态函数,以及页面中的 searchParams 属性,依赖于运行时传入的请求信息。使用这些功能将使路由退出完整路由缓存 (Full Route Cache),换言之,该路由将转为动态渲染。

cookies

在服务器操作 (Server Action) 中使用 cookies.setcookies.delete 会使路由缓存 (Router Cache) 失效,以防止使用 cookies 的路由变得过时(例如反映身份验证变更)。

更多信息请参阅 cookies API 参考文档

路由段配置选项 (Segment Config Options)

路由段配置选项可用于覆盖路由段的默认设置,或在无法使用 fetch API 时(如使用数据库客户端或第三方库)进行配置。

以下路由段配置选项将使数据缓存 (Data Cache) 和完整路由缓存 (Full Route Cache) 失效:

  • const dynamic = 'force-dynamic'
  • const revalidate = 0

更多选项请参阅 路由段配置文档

generateStaticParams

对于动态路由段 (dynamic segments)(如 app/blog/[slug]/page.js),由 generateStaticParams 提供的路径会在构建时缓存到完整路由缓存 (Full Route Cache) 中。在请求时,Next.js 还会在首次访问时缓存构建时未知的路径。

您可以通过在路由段中使用 export const dynamicParams = false 选项来禁用请求时的缓存。使用此配置后,仅会提供由 generateStaticParams 生成的路径,其他路由将返回 404 或匹配(对于全捕获路由 (catch-all routes))。

更多信息请参阅 generateStaticParams API 参考文档

React cache 函数

React 的 cache 函数允许您对函数的返回值进行记忆化 (memoize),从而在多次调用同一函数时仅执行一次。

由于 fetch 请求会自动记忆化,您无需将其包裹在 React cache 中。但在 fetch API 不适用的情况下(如某些数据库客户端、CMS 客户端或 GraphQL 客户端),您可以使用 cache 手动记忆化数据请求。

import { cache } from 'react'
import db from '@/lib/db'

export const getItem = cache(async (id: string) => {
  const item = await db.item.findUnique({ id })
  return item
})