跳到内容

Next.js 中的缓存

Next.js 通过缓存渲染工作和数据请求来提高应用程序的性能并降低成本。本页将深入探讨 Next.js 的缓存机制、可用于配置它们的 API 以及它们如何相互作用。

须知事项:本页旨在帮助您了解 Next.js 的内部工作原理,但它对于高效使用 Next.js **并非**必备知识。Next.js 的大多数缓存启发式算法都由您的 API 使用方式决定,并具有零配置或最小配置即可获得最佳性能的默认值。如果您想直接查看示例,请从这里开始

概述

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

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

默认情况下,Next.js 将尽可能多地缓存以提高性能并降低成本。这意味着除非您选择退出,否则路由是**静态渲染**的,数据请求是**缓存**的。下图显示了默认的缓存行为:路由何时在构建时静态渲染以及静态路由首次访问时。

Diagram showing the default caching behavior in Next.js for the four mechanisms, with HIT, MISS and SET at build time and when a route is first visited.

缓存行为因路由是静态渲染还是动态渲染、数据是缓存还是未缓存,以及请求是初始访问还是后续导航而异。根据您的用例,您可以为单个路由和数据请求配置缓存行为。

在 `proxy` 中**不支持** `fetch` 缓存。在 `proxy` 中进行的任何 `fetch` 都将不被缓存。

渲染策略

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

静态渲染

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

动态渲染

通过动态渲染,路由在**请求时**渲染。当您的路由使用请求特定信息(例如 cookie、标头或搜索参数)时,就会发生这种情况。

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

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

须知事项:您可以使用缓存组件在同一路由中混合静态和动态渲染。

请求记忆化

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

Deduplicated Fetch Requests

例如,如果您需要在路由中(例如在布局、页面和多个组件中)使用相同的数据,则无需在树的顶部获取数据,并在组件之间转发 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

请求记忆化如何工作

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

须知:

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

持续时间

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

重新验证

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

退出缓存

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

要管理单个请求,您可以使用 `signal` 属性,它来自 `AbortController`

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) 重用,并且缓存选项在硬刷新时被忽略。

数据缓存如何工作

Diagram showing how cached and uncached fetch requests interact with the Data Cache. Cached requests are stored in the Data Cache, and memoized, uncached requests are fetched from the data source, not stored in the Data Cache, and memoized.
  • 在渲染期间首次调用带有 `'force-cache'` 选项的 `fetch` 请求时,Next.js 会检查数据缓存中是否有缓存响应。
  • 如果找到缓存响应,则立即返回并记忆化
  • 如果未找到缓存响应,则向数据源发出请求,结果存储在数据缓存中,并进行记忆化。
  • 对于未缓存的数据(例如未定义 `cache` 选项或使用 `{ cache: 'no-store' }`),结果总是从数据源获取并记忆化。
  • 无论数据是否缓存,请求始终会被记忆化,以避免在 React 渲染过程中为相同数据发出重复请求。

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

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

持续时间

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

重新验证

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

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

基于时间的重新验证

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

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

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

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

Diagram showing how time-based revalidation works, after the revalidation period, stale data is returned for the first request, then data is revalidated.
  • 首次调用带有 `revalidate` 的 fetch 请求时,数据将从外部数据源获取并存储在数据缓存中。
  • 在指定时间范围内(例如 60 秒)调用的任何请求都将返回缓存数据。
  • 时间范围过后,下一个请求仍将返回缓存的(现在已过时)数据。
    • Next.js 将在后台触发数据重新验证。
    • 一旦数据成功获取,Next.js 将使用新鲜数据更新数据缓存。
    • 如果后台重新验证失败,则之前的数据将保持不变。

这类似于**陈旧时重新验证**的行为。

按需重新验证

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

按需重新验证如何工作

Diagram showing how on-demand revalidation works, the Data Cache is updated with fresh data after a revalidation request.
  • 第一次调用 `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 服务器组件 Payload**。
  2. Next.js 使用 React 服务器组件 Payload 和客户端组件 JavaScript 指令在服务器上渲染 **HTML**。

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

什么是 React 服务器组件 Payload?

React 服务器组件 Payload 是渲染后的 React 服务器组件树的紧凑二进制表示。它被客户端的 React 用于更新浏览器的 DOM。React 服务器组件 Payload 包含

  • 服务器组件的渲染结果
  • 客户端组件应在何处渲染的占位符以及对其 JavaScript 文件的引用
  • 从服务器组件传递到客户端组件的任何 props

要了解更多信息,请参阅服务器组件文档。

2. 服务器上的 Next.js 缓存(完整路由缓存)

Default behavior of the Full Route Cache, showing how the React Server Component Payload and HTML are cached on the server for statically rendered routes.

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

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

在请求时,在客户端上

  1. HTML 用于立即显示客户端和服务器组件的快速非交互式初始预览。
  2. React 服务器组件 Payload 用于协调客户端和渲染后的服务器组件树,并更新 DOM。
  3. JavaScript 指令用于水合客户端组件并使应用程序具有交互性。

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

React 服务器组件 Payload 存储在客户端路由缓存中——一个单独的内存缓存,按独立的路由段(布局、加载状态和页面)进行划分。此路由缓存用于通过存储以前访问的路由和预取未来的路由来改善导航体验。

5. 后续导航

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

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

静态渲染与动态渲染

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

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

How static and dynamic rendering affects the Full Route Cache. Static routes are cached at build time or after data revalidation, whereas dynamic routes are never cached

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

持续时间

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

失效

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

  • **重新验证数据**:重新验证数据缓存,反过来会通过在服务器上重新渲染组件并缓存新的渲染输出来使路由缓存失效。
  • **重新部署**:与数据缓存不同(数据缓存跨部署持久),完整路由缓存在新部署时会被清除。

退出缓存

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

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

客户端路由缓存

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

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

通过路由缓存

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

须知事项:此缓存专门适用于 Next.js 和服务器组件,与浏览器的bfcache不同,尽管它们有相似的结果。

持续时间

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

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

虽然页面刷新将清除**所有**缓存段,但自动失效期仅影响自预取时起的单个段。

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

失效

您可以通过两种方式使路由缓存失效:

退出缓存

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

须知事项:您还可以通过将 `` 组件的 `prefetch` 属性设置为 `false` 来选择退出预取

缓存交互

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

数据缓存和完整路由缓存

  • 重新验证或退出数据缓存**将**使完整路由缓存失效,因为渲染输出依赖于数据。
  • 使完整路由缓存失效或选择退出**不会**影响数据缓存。您可以动态渲染具有缓存和未缓存数据的路由。当您的大部分页面使用缓存数据,但有一些组件依赖需要在请求时获取的数据时,这会很有用。您可以动态渲染,而无需担心重新获取所有数据带来的性能影响。

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

  • 要立即使数据缓存和路由缓存失效,您可以在服务器操作中使用`revalidatePath``revalidateTag`
  • 路由处理程序中重新验证数据**不会**立即使路由缓存失效,因为路由处理程序未绑定到特定路由。这意味着路由缓存将继续提供之前的 Payload,直到硬刷新或自动失效期结束。

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重新验证 (Server Action)重新验证重新验证
revalidatePath重新验证 (Server Action)重新验证重新验证
const revalidate重新验证或退出缓存重新验证或退出缓存
const dynamic缓存或退出缓存缓存或退出缓存
cookies重新验证 (Server Action)退出缓存
headers, searchParams退出缓存
generateStaticParams缓存
React.cache缓存
unstable_cache缓存

默认情况下,`<Link>` 组件会自动从完整路由缓存中预取路由,并将 React 服务器组件 Payload 添加到路由缓存中。

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

了解更多关于`<Link>` 组件的信息。

router.prefetch

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

请参阅`useRouter` 钩子 API 参考。

router.refresh

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

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

请参阅`useRouter` 钩子 API 参考。

fetch

`fetch` 返回的数据**不会**自动缓存到数据缓存中。

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

有关更多选项,请参阅`fetch` API 参考

fetch options.cache

您可以将单个 `fetch` 请求通过将 `cache` 选项设置为 `force-cache` 来选择加入缓存。

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

有关更多选项,请参阅`fetch` API 参考

fetch options.next.revalidate

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

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

有关更多选项,请参阅`fetch` API 参考

fetch options.next.tagsrevalidateTag

Next.js 具有用于精细数据缓存和重新验证的缓存标记系统。

  1. 使用 `fetch` 或 `unstable_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. 路由处理程序 - 重新验证数据以响应第三方事件(例如 webhook)。这不会立即使路由缓存失效,因为路由处理程序未绑定到特定路由。
  2. 服务器操作 - 在用户操作(例如表单提交)后重新验证数据。这会使相关路由的路由缓存失效。

revalidatePath

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

revalidatePath('/')

您可以根据要实现的目标在两个地方使用 `revalidatePath`:

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

有关更多信息,请参阅`revalidatePath` API 参考

**`revalidatePath`** 与 **`router.refresh`**

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

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

动态 API

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

cookies

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

请参阅`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())
 
  // 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/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
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
})