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 状态

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

请求记忆化

Next.js 扩展了 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() // 缓存 MISS

// 第二次调用可以出现在路由的任何位置
const item = await getItem() // 缓存 HIT
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 组件树,这意味着:
    • 它适用于 generateMetadatagenerateStaticParams、布局、页面和其他服务端组件中的 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 内置数据缓存,可跨入站服务端请求部署持久化数据获取结果。这是通过扩展原生 fetch API 实现的,允许服务端上的每个请求设置自己的持久化缓存语义。

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

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

数据缓存工作原理

展示缓存和非缓存 fetch 请求如何与数据缓存交互的示意图。缓存请求存储在数据缓存中并被记忆化,非缓存请求从数据源获取,不存储在数据缓存中,但被记忆化。
  • 渲染期间首次调用带 'force-cache' 选项的 fetch 请求时,Next.js 会检查数据缓存中是否有缓存响应。
  • 如果找到缓存响应,则立即返回并记忆化
  • 如果未找到缓存响应,则向数据源发起请求,结果存储在数据缓存中并被记忆化。
  • 对于非缓存数据(如未定义 cache 选项或使用 { cache: 'no-store' }),结果总是从数据源获取并被记忆化。
  • 无论数据是否缓存,请求总是被记忆化以避免在 React 渲染过程中对相同数据发起重复请求。

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

虽然两种缓存机制都通过重用缓存数据提升性能,但数据缓存在入站请求和部署间持久化,而记忆化仅持续单次请求的生命周期。

持续时间

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

重新验证

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

  • 基于时间的重新验证:在一定时间后且发起新请求时重新验证数据。适用于变化不频繁且新鲜度要求不高的数据。
  • 按需重新验证:基于事件(如表单提交)重新验证数据。按需重新验证可使用基于标签或路径的方法一次性重新验证数据组。适用于需要尽快显示最新数据的场景(如无头 CMS 内容更新时)。

基于时间的重新验证

要按时间间隔重新验证数据,可使用 fetchnext.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调整上述自动失效时间。

失效机制

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

退出机制

自 Next.js 15 起,页面段默认退出缓存。

须知:您也可以通过将<Link>组件的prefetch属性设为false退出预取

缓存交互

配置不同缓存机制时,理解它们之间的交互关系至关重要:

数据缓存与完整路由缓存

  • 重新验证或退出数据缓存使完整路由缓存失效,因为渲染输出依赖于数据。
  • 使完整路由缓存失效或退出不会影响数据缓存。您可以动态渲染同时包含缓存和非缓存数据的路由。这在页面主体使用缓存数据,但部分组件需要实时获取数据时非常有用。

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

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

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>组件会自动从完整路由缓存预取路由,并将服务端组件负载添加到路由缓存中。

prefetch属性设为false可禁用预取,但这不会永久跳过缓存——用户访问路由时仍会在客户端缓存该路由段。

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

router.prefetch

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

参见useRouter钩子API参考。

router.refresh

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

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

参见useRouter钩子API参考。

fetch

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

未指定cache选项时,fetch的默认缓存行为等同于设置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选项可设置单个请求的重新验证周期(秒)。这会重新验证数据缓存,进而重新验证完整路由缓存。将获取新数据并在服务端重新渲染组件。

// 最多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

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

参见revalidatePath API参考获取更多信息。

revalidatePath vs. router.refresh:

调用router.refresh会清除路由缓存,并在服务端重新渲染路由段,但不影响数据缓存和完整路由缓存。

关键区别在于revalidatePath会清空数据缓存和完整路由缓存,而router.refresh()作为客户端API不会改变这两者。

动态API

cookiesheaders等动态API以及页面中的searchParams属性依赖于运行时请求信息。使用它们会使路由退出完整路由缓存(即动态渲染)。

cookies

在服务端操作中使用cookies.setcookies.delete会使路由缓存失效,防止使用cookie的路由过期(如反映认证变更)。

参见cookies API参考。

路由段配置选项

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

以下路由段配置选项将退出全路由缓存:

  • const dynamic = 'force-dynamic'

此配置选项将使所有数据获取退出数据缓存(即 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 函数允许您记忆化函数的返回值,使得多次调用同一函数时仅执行一次。

由于 fetch 请求会自动记忆化,您无需用 React cache 包裹它。但当 fetch API 不适用时,您可以使用 cache 手动记忆化数据请求。例如某些数据库客户端、CMS 客户端或 GraphQL 客户端。

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
})
import { cache } from 'react'
import db from '@/lib/db'

export const getItem = cache(async (id) => {
  const item = await db.item.findUnique({ id })
  return item
})