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() // cache MISS

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

请求记忆化工作原理

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

须知

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

持续时间

缓存持续到服务端请求生命周期结束,即 React 组件树完成渲染时。

重新验证

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

选择退出

要在 fetch 请求中退出记忆化,可以向请求传递 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 的情况下配置。

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

展示基于时间重新验证工作原理的示意图,重新验证周期后,首次请求返回过期数据,随后数据被重新验证。
  • 首次调用带 revalidatefetch 请求时,数据从外部数据源获取并存储在数据缓存中。
  • 在指定时间范围内(如 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'

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 服务端组件负载协调客户端和已渲染的服务端组件树,并更新 DOM。
  3. 使用 JavaScript 指令水合客户端组件,使应用可交互。

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

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

5. 后续导航

在后续导航或预取过程中,Next.js 会检查路由缓存 (Router Cache) 中是否已存储 React 服务端组件负载 (React Server Components Payload)。若存在,则会跳过向服务器发送新请求。

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

静态与动态渲染

路由是否在构建时被缓存取决于其采用静态渲染 (Static Rendering) 还是动态渲染 (Dynamic Rendering)。静态路由默认会被缓存,而动态路由则在请求时渲染且不被缓存。

下图展示了静态与动态渲染路由的区别,以及缓存与未缓存数据的差异:

静态与动态渲染对全路由缓存 (Full Route Cache) 的影响。静态路由在构建时或数据重新验证后被缓存,而动态路由从不缓存

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

持续时间

默认情况下,全路由缓存是持久化的。这意味着渲染输出会在用户请求间持续缓存。

失效机制

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

退出机制

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

  • 使用动态函数 (Dynamic Function):这将使路由退出全路由缓存,并在请求时动态渲染。数据缓存仍可被使用。
  • 使用路由段配置选项 dynamic = 'force-dynamic'revalidate = 0:这会跳过全路由缓存和数据缓存。意味着每次传入请求时都会在服务端渲染组件并获取数据。路由缓存仍会生效,因为它是客户端缓存。
  • 退出数据缓存 (Data Cache):如果路由包含未被缓存的 fetch 请求,该路由将退出全路由缓存。针对该特定 fetch 请求的数据会在每次传入请求时获取。其他未退出缓存的 fetch 请求仍会被保存在数据缓存中。这允许混合使用缓存与非缓存数据。

路由缓存

相关术语:

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

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

路由缓存工作原理

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

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

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

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

路由缓存与全路由缓存的区别

路由缓存临时在浏览器中存储 React 服务端组件负载(用户会话期间),而全路由缓存在服务器端持久化存储 React 服务端组件负载和 HTML(跨用户请求)。

全路由缓存仅缓存静态渲染的路由,而路由缓存同时适用于静态和动态渲染的路由。

持续时间

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

  • 会话 (Session):缓存在导航间持续存在,但页面刷新时会被清除。
  • 自动失效周期 (Automatic Invalidation Period):单个路由段的缓存会在特定时间后自动失效。持续时间取决于路由是静态还是动态渲染:
    • 动态渲染:30 秒
    • 静态渲染:5 分钟

页面刷新会清除所有缓存的路由段,而自动失效周期仅影响自上次访问或创建后的单个路由段。

通过为动态渲染的路由添加 prefetch={true} 或调用 router.prefetch,您可以选择将缓存时间延长至 5 分钟。

失效机制

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

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

退出机制

无法完全退出路由缓存。

您可以通过将 <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, useSearchParams, 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)

cookiesheadersuseSearchParamssearchParams 都是依赖运行时请求信息的动态函数。使用这些函数会使路由退出完整路由缓存 (Full Route Cache),即该路由将转为动态渲染。

cookies

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

参阅 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
})

unstable_cache

unstable_cache 是一个实验性 API,用于在不适合使用 fetch API 时(如使用数据库客户端、CMS 客户端或 GraphQL)向数据缓存 (Data Cache) 添加值。

import { unstable_cache } from 'next/cache'

export default async function Page() {
  const cachedData = await unstable_cache(
    async () => {
      const data = await db.query('...')
      return data
    },
    ['cache-key'],
    {
      tags: ['a', 'b', 'c'],
      revalidate: 10,
    }
  )()
}

警告:此 API 正在开发中,我们不建议在生产环境中使用。此处列出是为了展示数据缓存的未来发展方向。