Menu

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。

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

proxy 中不支持 Fetch 缓存。在你的 proxy 中执行的任何 fetch 都不会被缓存。

渲染策略

要理解 Next.js 中缓存的工作原理,了解可用的渲染策略很有帮助。渲染策略决定了何时生成路由的 HTML,这直接影响可以缓存的内容。

静态渲染

使用静态渲染,路由在构建时或在数据重新验证后在后台渲染。结果被缓存并可以跨请求重用。静态路由完全缓存在完整路由缓存中。

动态渲染

使用动态渲染,路由在请求时渲染。当你的路由使用请求特定信息(如 cookies、headers 或搜索参数)时会发生这种情况。

当路由使用以下任何 API 时,它会变为动态:

动态路由不会缓存在完整路由缓存中,但仍可以使用数据缓存来缓存数据请求。

值得注意的是:你可以使用缓存组件在同一路由中混合静态和动态渲染。

请求记忆化

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

去重的 Fetch 请求

例如,如果你需要在整个路由中使用相同的数据(例如在布局、页面和多个组件中),你不必在树的顶部获取数据并在组件之间转发 props。相反,你可以在需要数据的组件中获取数据,而无需担心为相同数据在网络上发出多个请求的性能影响。

app/example.tsx
TypeScript
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 组件树,这意味着:
    • 它适用于 generateMetadata、generateStaticParams、布局、页面和其他服务器组件中的 fetch 请求。
    • 它不适用于路由处理程序中的 fetch 请求,因为它们不是 React 组件树的一部分。
  • 对于不适合使用 fetch 的情况(例如某些数据库客户端、CMS 客户端或 GraphQL 客户端),你可以使用 React cache 函数来记忆化函数。

持续时间

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

重新验证

由于记忆化不会在服务器请求之间共享,并且仅在渲染期间应用,因此无需重新验证它。

选择退出

记忆化仅适用于 fetch 请求中的 GET 方法,其他方法(如 POST 和 DELETE)不会被记忆化。这种默认行为是 React 的优化,我们不建议选择退出。

要管理单个请求,你可以使用 AbortController 的 signal 属性。

app/example.js
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),缓存选项会被忽略以进行硬刷新。

数据缓存的工作原理

显示缓存和未缓存的 fetch 请求如何与数据缓存交互的图表。缓存的请求存储在数据缓存中并被记忆化,未缓存的请求从数据源获取,不存储在数据缓存中,但被记忆化。
  • 在渲染期间第一次调用带有 '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 边界。

每个块分两步渲染:

  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 将检查 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 配置选项可用于调整上述自动失效时间。

失效

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

  • 在服务器操作中:
  • 调用 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> 组件自动从完整路由缓存预取路由,并将 React 服务器组件负载添加到路由器缓存。

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

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

router.prefetch

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

查看 useRouter 钩子 API 参考。

router.refresh

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

渲染的结果将在客户端上协调,同时保留 React 状态和浏览器状态。

查看 useRouter 钩子 API 参考。

fetch

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

默认情况下,当未提供 cache 或 next.revalidate 选项时:

查看 fetch API 参考以获取更多选项。

fetch options.cache

你可以通过将 cache 选项设置为 force-cache 来选择将单个 fetch 加入缓存:

// 选择加入缓存
fetch(`https://...`, { cache: 'force-cache' })

查看 fetch API 参考以获取更多选项。

fetch options.next.revalidate

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

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

查看 fetch API 参考以获取更多选项。

fetch options.next.tags 和 revalidateTag

Next.js 有一个缓存标记系统,用于细粒度的数据缓存和重新验证。

  1. 当使用 fetch 或 unstable_cache 时,你可以选择用一个或多个标签标记缓存条目。
  2. 然后,你可以调用 revalidateTag 来清除与该标签关联的缓存条目。

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

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

然后,使用标签调用 revalidateTag 来清除缓存条目:

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

根据你想要实现的目标,有两个地方可以使用 revalidateTag:

  1. 路由处理程序 - 响应第三方事件(例如 webhook)重新验证数据。这不会立即使路由器缓存失效,因为路由处理程序没有绑定到特定路由。
  2. 服务器操作 - 在用户操作(例如表单提交)后重新验证数据。这将使关联路由的路由器缓存失效。

revalidatePath

revalidatePath 允许你在单个操作中手动重新验证数据并重新渲染特定路径下方的路由段。调用 revalidatePath 方法会重新验证数据缓存,进而使完整路由缓存失效。

revalidatePath('/')

根据你想要实现的目标,有两个地方可以使用 revalidatePath:

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

查看 revalidatePath API 参考以获取更多信息。

revalidatePath vs. router.refresh:

调用 router.refresh 将清除路由器缓存,并在服务器上重新渲染路由段,而不使数据缓存或完整路由缓存失效。

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

动态 API

像 cookies 和 headers 这样的动态 API,以及页面中的 searchParams 属性依赖于运行时传入的请求信息。使用它们会使路由退出完整路由缓存,换句话说,路由将被动态渲染。

cookies

在服务器操作中使用 cookies.set 或 cookies.delete 会使路由器缓存失效,以防止使用 cookies 的路由变得陈旧(例如反映身份验证更改)。

查看 cookies API 参考。

路由段配置选项

路由段配置选项可用于覆盖路由段默认值,或当你无法使用 fetch API 时(例如数据库客户端或第三方库)。

以下路由段配置选项将选择退出完整路由缓存:

  • const dynamic = 'force-dynamic'

此配置选项将选择退出所有 fetch 的数据缓存(即 no-store):

  • const fetchCache = 'default-no-store'

查看 fetchCache 以了解更多高级选项。

查看路由段配置文档以获取更多选项。

generateStaticParams

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

要在构建时静态渲染所有路径,请向 generateStaticParams 提供完整的路径列表:

app/blog/[slug]/page.js
export async function generateStaticParams() {
  const posts = await fetch('https://.../posts').then((res) => res.json())
 
  return posts.map((post) => ({
    slug: post.slug,
  }))
}

要在构建时静态渲染路径的子集,其余的在运行时首次访问时渲染,返回部分路径列表:

app/blog/[slug]/page.js
export async function generateStaticParams() {
  const posts = await fetch('https://.../posts').then((res) => res.json())
 
  // 在构建时渲染前 10 篇文章
  return posts.slice(0, 10).map((post) => ({
    slug: post.slug,
  }))
}

要在首次访问时静态渲染所有路径,返回一个空数组(在构建时不会渲染任何路径)或使用 export const dynamic = 'force-static':

app/blog/[slug]/page.js
export async function generateStaticParams() {
  return []
}

值得注意的是:你必须从 generateStaticParams 返回一个数组,即使它是空的。否则,路由将被动态渲染。

app/changelog/[slug]/page.js
export const dynamic = 'force-static'

要在请求时禁用缓存,在路由段中添加 export const dynamicParams = false 选项。使用此配置选项时,只有 generateStaticParams 提供的路径将被提供,其他路由将 404 或匹配(在捕获所有路由的情况下)。

React cache 函数

React cache 函数允许你记忆化函数的返回值,允许你多次调用相同的函数,但只执行一次。

使用 GET 或 HEAD 方法的 fetch 请求会自动记忆化,因此你不需要将其包装在 React cache 中。但是,对于其他 fetch 方法,或者当使用不固有记忆化请求的数据获取库(如某些数据库、CMS 或 GraphQL 客户端)时,你可以使用 cache 手动记忆化数据请求。

utils/get-item.ts
TypeScript
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
})