Skip to content

缓存

耶和博

6900字约23分钟

2024-08-10

Next.js 中的缓存

Next.js 通过缓存渲染工作和数据请求来提高应用程序的性能并降低成本。本页面提供了对 Next.js 缓存机制的深入了解,您可以使用哪些 API 来配置它们,以及它们如何相互作用。

提示

本页面帮助您了解 Next.js 的工作原理,但不是必需的知识来使用 Next.js。大多数 Next.js 的缓存启发式是由您的 API 使用决定的,并且具有零或最小配置的最佳性能的默认值。如果您想直接跳转到示例,请从这里开始


概述

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

机制内容位置目的持续时间
Request Memoization函数的返回值服务器在 React 组件树中重用数据请求生命周期
Data Cache数据服务器跨用户请求和部署存储数据持久化(可以重新验证)
Full Route CacheHTML 和 RSC 负载服务器减少渲染成本并提高性能持久化(可以重新验证)
Router CacheRSC Payload客户端减少导航时的服务器请求用户会话或时间

默认情况下,Next.js 会尽可能缓存以提高性能并减少成本。这意味着路由是静态渲染的,数据请求是缓存的,除非您选择不缓存。下图显示了默认的缓存行为:当在构建时静态渲染路由并在首次访问静态路由时。

显示 Next.js 四种机制默认缓存行为的图表,在构建时和首次访问路由时显示 HIT、MISS 和 SET。

缓存行为取决于路由是静态渲染还是动态渲染,数据是缓存还是未缓存,以及请求是否是初始访问的一部分还是后续导航的一部分。根据您的用例,您可以为各个路由和数据请求配置缓存行为。


请求记忆化

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

重复数据删除的 Fetch 请求

例如,如果您需要在一个路由中跨多个组件使用相同的数据(例如,在 Layout、Page 和多个组件中),您不必在树的顶部获取数据,并在此处转发 props 在组件之间。相反,您可以在需要数据的组件中获取数据,而无需担心跨网络多次请求相同数据的性能影响。

app/example.tsx
async function getItem() {
  // The `fetch` function is automatically memoized and the result
  // is cached
  const res = await fetch('https://.../item/1')
  return res.json()
}
 
// This function is called twice, but only executed the first time
const item = await getItem() // cache MISS
 
// The second call could be anywhere in your route
const item = await getItem() // cache HIT

请求记忆化的工作原理

显示 fetch 记忆化在 React 渲染期间如何工作的图表

  • 在渲染路由时,第一次调用特定请求时,其结果将不在内存中,因此它将是缓存 MISS
  • 因此,函数将执行,并将从外部源获取数据,并将结果存储在内存中。
  • 在同一渲染过程中对请求的后续函数调用将是缓存 HIT,并且数据将从内存中返回,而无需执行函数。
  • 一旦路由被渲染并且渲染过程完成,内存就会被“重置”,并且所有请求记忆化条目都会被清除。

提示

  • 请求记忆化是 React 功能,而不是 Next.js 功能。在这里包括它是为了显示它与其他缓存机制的交互。
  • 请求记忆化仅适用于 fetch 请求的 GET 方法。
  • 请求记忆化仅适用于 React 组件树,这意味着:
    • 它适用于 generateMetadatagenerateStaticParams、布局、页面和其他 Server Components 中的 fetch 请求。
    • 它不适用于 Route Handlers 中的 fetch 请求,因为它们不是 React 组件树的一部分。
    • 对于不适合 fetch 的情况(例如某些数据库客户端、CMS 客户端或 GraphQL 客户端),您可以使用 React cache 函数 来记忆化函数。

持续时间

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

重新验证

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

选择退出

请求记忆化仅适用于 fetch 请求的 GET 方法,其他方法,如 POSTDELETE,不记忆化。这种默认行为是一个 React 优化,我们不建议选择退出。

要管理单个请求,您可以使用 signal 属性从 AbortController。然而,这不会选择退出请求记忆化,而是中止正在进行的请求。

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

数据缓存

Next.js 有一个内置的数据缓存,它持久化数据请求的结果,跨服务器请求和部署。这是可能的,因为 Next.js 扩展了原生的 fetch API,以允许每个服务器请求设置其自己的持久缓存语义。

提示

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

您可以使用 cachenext.revalidate 选项的 fetch 来配置缓存行为。

数据缓存的工作原理

显示缓存和未缓存的 fetch 请求如何与数据缓存交互的图表。缓存的请求存储在数据缓存中并被记忆化,未缓存的请求从数据源获取,不存储在数据缓存中,并被记忆化。

  • 第一次在渲染期间调用带有 'force-cache' 选项的 fetch 请求时,Next.js 会检查数据缓存中是否已缓存响应。
  • 如果找到缓存响应,它将立即返回并被记忆化。
  • 如果没有找到缓存响应,请求将发送到数据源,结果将存储在数据缓存中,并被记忆化。
  • 对于未缓存的数据(例如,未定义 cache 选项或使用 { cache: 'no-store' }),结果总是从数据源获取,并被记忆化。
  • 无论数据是否缓存,请求总是被记忆化,以避免在 React 渲染期间对相同数据进行重复请求。

数据缓存和请求记忆化之间的区别

虽然两种缓存机制都通过重用缓存数据来帮助提高性能,但数据缓存在传入请求和部署之间是持久的,而请求记忆化仅在请求的生命周期内持续。

持续时间

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

重新验证

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

  • 基于时间的重新验证: 在经过一定时间并发出新请求后重新验证数据。这对于不经常变化且新鲜度不那么重要的数据很有用。
  • 按需重新验证: 基于事件(例如表单提交)重新验证数据。按需重新验证可以使用基于标签或基于路径的方法一次重新验证一组数据。当您希望确保尽快显示最新数据时(例如,当您的无头 CMS 中的内容更新时),这很有用。

基于时间的重新验证

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

// Revalidate at most every hour
fetch('https://...', { next: { revalidate: 3600 } })

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

基于时间的重新验证如何工作

显示基于时间的重新验证如何工作的图表,在重新验证期之后,第一个请求返回过时数据,然后数据被重新验证。

  • 第一次调用带有 revalidatefetch 请求时,数据将从外部数据源获取并存储在数据缓存中。
  • 任何在指定时间段内调用的请求将返回缓存的数据。
  • 在时间段之后,下一个请求将仍然返回缓存(现在过时)的数据。
    • Next.js 将在后台触发数据的重新验证。
    • 一旦数据成功获取,Next.js 将使用新鲜数据更新数据缓存。
    • 如果后台重新验证失败,则保留先前的数据。

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

按需重新验证

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

按需重新验证如何工作

显示按需重新验证如何工作的图表,在重新验证请求后,数据缓存用新鲜数据更新。

  • 第一次调用 fetch 请求时,数据将从外部数据源获取并存储在数据缓存中。
  • 当按需重新验证被触发时,适当的缓存条目将从缓存中清除。
    • 这与基于时间的重新验证不同,后者在获取新鲜数据之前将保持过时数据在缓存中。
  • 下一次请求时,它将再次成为缓存 MISS,并且数据将从外部数据源获取并存储在数据缓存中。

选择退出

如果您不希望缓存 fetch 的响应,您可以执行以下操作:

let data = await fetch('https://api.vercel.app/blog', { cache: 'no-store' })

全路由缓存

提示

相关术语:

您可能会看到术语 Automatic Static OptimizationStatic Site GenerationStatic Rendering 被互换使用,以指代在构建时渲染和缓存应用程序路由的过程。

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

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

1. React 在服务器上的渲染

在服务器上,Next.js 使用 React 的 API 来协调渲染。渲染工作分为小块:按单个路由段和 Suspense 边界。

每块渲染分为两步:

  1. React 将 Server Components 渲染为一种特殊的数据格式,这种格式针对流式传输进行了优化,称为 React Server Component Payload
  2. Next.js 使用 React Server Component Payload 和 Client Component JavaScript 指令在服务器上渲染 HTML

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

什么是 React 服务器组件有效负载?

React Server Component Payload 是渲染的 React Server Components 树的紧凑二进制表示。它在客户端由 React 使用,以更新浏览器的 DOM。React Server Component Payload 包含:

  • Server Components 的渲染结果
  • 客户端渲染 Client Components 的占位符和 JavaScript 文件引用
  • 从 Server Component 传递到 Client Component 的 props

要了解更多信息,请参阅 Server Components 文档。

2. Next.js 在服务器上的缓存 (全路由缓存)

完整路由缓存的默认行为,显示 React 服务器组件有效负载和 HTML 如何在服务器上为静态渲染的路由缓存。

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

3. React 在客户端的水合和协调

在请求时,在客户端:

  1. HTML 用于立即显示 Client 和 Server Components 的快速非交互初始预览。
  2. React Server Components Payload 用于调和 Client 和 rendered Server Component 树,并更新 DOM。
  3. JavaScript 指令用于 hydrate Client Components 并使应用程序交互。

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

React 服务器组件有效负载存储在客户端的 路由缓存 - 一个单独的内存缓存,按单个路由段分割。这个 Router Cache 用于通过存储先前访问的路由和预取未来路由来改善导航体验。

5. 后续导航

在后续导航或预取期间,Next.js 将检查 React 服务器组件有效负载是否存储在 Router Cache 中。如果是,它将跳过向服务器发送新请求。

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

静态和动态渲染

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

此图显示了静态和动态渲染的路由之间的区别,以及缓存和未缓存的数据:

静态和动态渲染如何影响完整路由缓存。静态路由在构建时或数据重新验证后缓存,而动态路由永远不会缓存

了解更多关于静态和动态渲染的信息。

持续时间

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

失效

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

  • 重新验证数据: 重新验证数据缓存将反过来使路由器缓存失效,通过在服务器上重新渲染组件并缓存新的渲染输出。
  • 重新部署: 与数据缓存不同,路由器缓存在部署之间是持久的,通过在服务器上重新渲染组件并缓存新的渲染输出。
选择退出

您可以通过以下方式选择退出完整路由缓存,换句话说,为每个传入请求动态渲染组件:

  • 使用动态函数:这将使路由从完整路由缓存中选择退出,并在请求时动态渲染它。数据缓存仍然可以使用。
  • 使用 dynamic = 'force-dynamic'revalidate = 0 路由段配置选项:这将选择退出完整路由缓存和数据缓存。这意味着组件将在每次传入请求时渲染和获取数据。路由缓存仍然适用,因为它是一个客户端缓存。
  • 选择退出数据缓存:如果路由有一个未缓存的 fetch 请求,这将使路由从完整路由缓存中选择退出。特定 fetch 请求的数据将每次传入请求时获取。其他未选择退出缓存的 fetch 请求仍然会缓存到数据缓存中。这允许缓存和未缓存数据的混合。

客户端路由器缓存

Next.js 有一个内存中的客户端路由器缓存,它存储路由段的 RSC 有效负载,按布局、加载状态和页面分割。

当用户在路由之间导航时,Next.js 会缓存访问的路由段,并预取用户可能导航到的路由。这导致即时后退/前进导航,在导航之间没有全页重新加载,并保留 React 状态和浏览器状态。

使用路由器缓存:

  • 布局被缓存并在导航时重用(部分渲染)。
  • 加载状态在导航时被缓存并重用(即时加载状态)。
  • 页面默认不被缓存,但在浏览器后退和前进导航期间被重用。您可以通过使用实验性的 staleTimes 配置选项为页面片段启用缓存。

提示

这个缓存专门应用于 Next.js 和 Server Components,与浏览器的 bfcache 不同,尽管它有类似的效果。

持续时间

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

  • Session: 缓存跨导航持续存在。然而,它在页面刷新时被清除。
  • 自动失效期: 布局和加载状态的缓存在特定时间后自动失效。持续时间取决于资源如何预取,以及资源是否静态生成:
    • 默认预取(prefetch={null} 或未指定): 动态页面未缓存,静态页面缓存 5 分钟。
    • Full Prefetching (prefetch={true}router.prefetch): 5 分钟用于静态和动态页面。

虽然页面刷新会清除所有缓存段,但自动失效期仅在预取段时生效。

提示

实验性的 staleTimes 配置选项可以用来调整上面提到的自动失效时间。

失效

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

选择退出

从 Next.js 15 开始,页面段默认选择退出。

提示

您还可以通过将 <Link> 组件的 prefetch 属性设置为 false 来选择退出预取


缓存交互

在配置不同的缓存机制时,了解它们如何相互作用很重要:

数据缓存和全路由缓存

  • 重新验证或选择退出数据缓存将使全路由缓存失效,因为渲染输出取决于数据。
  • 选择退出全路由缓存不会影响数据缓存。您可以动态渲染具有缓存和未缓存数据的路线。这在大多数页面使用缓存数据,但您有几个组件依赖于需要请求时获取的数据时非常有用。您可以动态渲染,而不用担心重新获取所有数据的性能影响。

数据缓存和客户端路由器缓存

  • 要立即使数据缓存和路由器缓存失效,您可以使用 revalidatePathrevalidateTagServer Action 中。
  • 重新验证数据缓存在 Route Handler不会 立即使路由器缓存失效,因为 Route Handler 不与特定路由绑定。这意味着路由器缓存将继续为前一个有效负载提供服务,直到进行硬刷新,或者自动失效期已过。

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 hook 的 prefetch 选项可用于手动预取路由。这会将 React 服务器组件有效负载添加到路由缓存中。

了解更多关于 useRouter hook 的信息。

router.refresh

useRouter hook 的 refresh 选项可用于手动刷新路由。这将完全清除路由缓存,并使对当前路由的新请求。refresh 不会影响数据缓存或全路由缓存。

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

了解更多关于 useRouter hook 的信息。

fetch

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

如果您不想缓存 fetch 的响应,可以执行以下操作:

let data = await fetch('https://api.vercel.app/blog', { cache: 'no-store' })

了解更多关于 fetch API Reference 的信息。

fetch options.cache

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

// Opt into caching
fetch(`https://...`, { cache: 'force-cache' })

了解更多关于 fetch API Reference 的信息。

fetch options.next.revalidate

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

// Revalidate at most after 1 hour
fetch(`https://...`, { next: { revalidate: 3600 } })

了解更多关于 fetch API reference 的信息。

fetch options.next.tagsrevalidateTag

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

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

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

// Cache data with a tag
fetch(`https://...`, { next: { tags: ['a', 'b', 'c'] } })

然后,调用 revalidateTag 来清除与该标签相关的缓存条目:

// Revalidate entries with a specific tag
revalidateTag('a')

您可以在两个地方使用 revalidateTag,具体取决于您要实现的目标:

  1. Route Handlers - 在响应第三方事件时重新验证数据(例如 webhook)。这不会立即使路由缓存失效,因为路由处理器不绑定到特定路由。
  2. Server Actions - 在用户交互后重新验证数据(例如表单提交、点击按钮)。这将使与该路由相关的路由缓存失效。

revalidatePath

revalidatePath 允许您手动重新验证数据,并重新渲染特定路径下的所有路由段。调用 revalidatePath 方法重新验证数据缓存,这将反过来使完整路由缓存失效。

revalidatePath('/')

您可以在两个地方使用 revalidatePath,具体取决于您要实现的目标:

  1. Route Handlers - 在响应第三方事件时重新验证数据(例如 webhook)。
  2. Server Actions - 在用户交互后重新验证数据(例如表单提交、点击按钮)。

了解更多关于 revalidatePath API reference 的信息。

revalidatePath vs. router.refresh:

  • 调用 router.refresh 将清除路由缓存,并在服务器上重新渲染路由段,而不使数据缓存或完整路由缓存失效。
  • 不同之处在于 revalidatePath 清除数据缓存和完整路由缓存,而 router.refresh() 不会改变数据缓存和完整路由缓存,因为它是一个客户端 API。

动态函数

动态函数如 cookiesheaders,以及 Pages 中的 searchParams 属性依赖于运行时传入的请求信息。使用它们会使路由选择退出完整路由缓存,换句话说,路由将动态渲染。

cookies

使用 cookies.setcookies.delete 在 Server Action 中会使路由缓存失效,以防止使用 cookies 的路由过时(例如身份验证更改)。

了解更多关于 cookies 的 API.

段配置选项

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

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

  • const dynamic = 'force-dynamic'

此配置选项将使所有获取退出数据缓存(即 no-store):

  • const fetchCache = 'default-no-store'

了解更多关于 fetchCache 的更多高级选项.

了解更多关于 Route Segment Config 的更多选项.

generateStaticParams

对于 dynamic segments (e.g. 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())
 
  // Render the first 10 posts at build time
  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/blog/[slug]/page.js
export const dynamic = 'force-static'

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

React cache function

React cache 函数允许您缓存函数的返回值,这样您就可以在多次调用同一个函数时只执行一次。

由于 fetch 请求是自动缓存的,因此您不需要将它包装在 React cache 中。然而,您可以使用 cache 手动缓存数据请求,以在 fetch API 不适合的情况下使用。例如,一些数据库客户端、CMS 客户端或 GraphQL 客户端。

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