跳到主要内容

Next.js 中的缓存机制

Next.js 通过缓存渲染工作和数据请求来提升应用程序的性能并降低成本。本页深入介绍了 Next.js 的缓存机制、你可以用来配置它们的 API,以及它们之间的交互方式。

提示:本页帮助你了解 Next.js 的内部工作原理,但不是使用 Next.js 进行高效开发的必备知识。Next.js 的大部分缓存启发式算法由你的 API 使用情况决定,并为最佳性能提供了零配置或最小配置的默认设置。如果你想直接查看示例,请从这里开始

概述

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

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

默认情况下,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.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.

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

middleware不支持 fetch 缓存。在 middleware 内部进行的任何 fetch 请求都不会被缓存。

请求记忆化

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

Deduplicated Fetch RequestsDeduplicated Fetch Requests

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

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

// 这个函数被调用了两次,但只在第一次执行
const item = await getItem() // 缓存 MISS

// 第二次调用可能在你路由的任何地方
const item = await getItem() // 缓存 HIT

请求记忆化的工作原理

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

提示

  • 请求记忆化是 React 的功能,不是 Next.js 的功能。这里包含它是为了展示它如何与其他缓存机制交互。
  • 记忆化仅适用于 fetch 请求中的 GET 方法。
  • 记忆化仅适用于 React 组件树,这意味着:
    • 它适用于 generateMetadatagenerateStaticParams、Layouts、Pages 和其他服务端组件中的 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 有一个内置的数据缓存,可以持久化跨传入服务端请求部署的数据获取结果。这是因为 Next.js 扩展了原生 fetch API,允许服务端的每个请求设置自己的持久化缓存语义。

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

你可以使用 fetchcachenext.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.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 的内容更新时),这很有用。

基于时间的重新验证

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

// 最多每小时重新验证一次
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.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 将用新鲜数据更新数据缓存。
    • 如果后台重新验证失败,将保持之前的数据不变。

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

按需重新验证

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

按需重新验证的工作原理

Diagram showing how on-demand revalidation works, the Data Cache is updated with fresh data after a revalidation request.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 服务端组件载荷
  2. Next.js 使用 React 服务端组件载荷和客户端组件 JavaScript 指令在服务端渲染 HTML

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

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

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

  • 服务端组件的渲染结果
  • 客户端组件应该渲染的位置的占位符以及对其 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.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 服务端组件载荷和 HTML)。这适用于在构建时静态渲染的路由,或在重新验证期间。

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

在请求时,在客户端:

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

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

React 服务端组件载荷存储在客户端路由缓存中 - 一个单独的进程内缓存,按单个路由段分割。这个路由缓存用于通过存储之前访问的路由和预取未来路由来改善导航体验。

5. 后续导航

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

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

静态和动态渲染

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

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

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 cachedHow 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 载荷,按布局、加载状态和页面分割。

当用户在路由之间导航时,Next.js 缓存访问的路由段并预取用户可能导航到的路由。这实现了即时前进/后退导航、导航之间无完整页面重新加载,以及共享布局中浏览器状态和 React 状态的保持。

通过路由缓存:

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

提示:此缓存专门适用于 Next.js 和服务端组件,与浏览器的 bfcache 不同,尽管它有类似的结果。

持续时间

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

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

虽然页面刷新会清除所有缓存的段,但自动失效期只影响从预取时间开始的单个段。

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

失效

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

  • 服务端操作中:
  • 调用 router.refresh 将使路由缓存失效并为当前路由向服务端发出新请求。

选择退出

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

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

缓存交互

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

数据缓存和完整路由缓存

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

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

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

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

请参阅 useRouter hook API 参考。

router.refresh

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

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

请参阅 useRouter hook API 参考。

fetch

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

fetch 的默认缓存行为(例如,当未指定 cache 选项时)等于将 cache 选项设置为 no-store

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

请参阅 fetch API 参考了解更多选项。

fetch options.cache

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

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

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

revalidatePath

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

revalidatePath('/')

你可以使用 revalidatePath 的两个地方,取决于你想要实现的目标:

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

请参阅 revalidatePath API 参考了解更多信息。

revalidatePath vs. router.refresh

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

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

动态 API

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

cookies

在服务端操作中使用 cookies.setcookies.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())

// 在构建时渲染前 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 函数允许你记忆化函数的返回值,允许你多次调用同一个函数,但只执行一次。

使用 GETHEAD 方法的 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
})