Skip to main content

Next.js中的缓存

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

您需要知道: 此页面帮助您了解 Next.js 的底层工作原理,但这是使用 Next.js 高效开发所必需的知识。Next.js 的大多数缓存策略由您的 API 使用情况决定,并且默认配置即可实现最佳性能,无需或仅需极少的配置。

概述

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

MechanismWhatWherePurposeDuration
Request MemoizationReturn values of functionsServerRe-use data in a React Component treePer-request lifecycle
Data CacheDataServerStore data across user requests and deploymentsPersistent (can be revalidated)
Full Route CacheHTML and RSC payloadServerReduce rendering cost and improve performancePersistent (can be revalidated)
Router CacheRSC PayloadClientReduce server requests on navigationUser session or time-based

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

缓存行为会根据路由是静态渲染还是动态渲染、数据是否被缓存、以及请求是初次访问还是后续导航的一部分而有所不同。根据您的具体需求,您可以为各个路由和数据请求配置缓存行为。

请求记忆化

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

Deduplicated Fetch Requests

例如,如果您需要在路由中的多个地方使用相同的数据(例如在布局、页面和多个组件中),您无需在树的顶部获取数据并在组件之间传递属性。相反,您可以在需要数据的组件中直接获取数据,而无需担心由于对相同数据进行多次网络请求而带来的性能影响。

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

How Request Memoization Works

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属性。不过, 这不会使请求退出缓存, 而是中止正在进行的请求。

const { signal } = new AbortController();
fetch(url, { signal });

数据缓存

Next.js 内置了一个数据缓存,可 ** 持久保存 ** 传入服务器请求部署中的数据获取结果。这是因为 Next.js 扩展了原生的 fetch API,使每个服务器请求都可以设置自己的持久缓存语义。

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

默认情况下,使用fetch的数据请求不会被缓存。您可以使用fetchcachenext.revalidate选项来配置缓存行为。

数据缓存的工作原理

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 渲染过程中为相同数据发出重复请求。

数据缓存与请求缓存的区别

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

通过请求缓存,我们减少了在同一渲染过程中必须跨越从渲染服务器到数据缓存服务器(例如 CDN 或边缘网络)或数据源(例如数据库或 CMS)的网络边界的重复请求数量。通过数据缓存,我们减少了对原始数据源的请求数量。

持续时间

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

重新验证

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

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

基于时间的重新验证

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

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

Alternatively, you can use Route Segment Config options to configure all fetch requests in a segment or for cases where you're not able to use fetch. 或者,您可以使用路由段配置选项来配置段中的所有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 将使用最新数据更新数据缓存。
    • 如果后台重新验证失败,之前的数据将保持不变。

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

按需重新验证

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

按需重新验证工作原理

Diagram showing how on-demand revalidation works, the Data Cache is updated with fresh data after a revalidation request.
  • 首次调用fetch请求时,数据将从外部数据源获取并存储在数据缓存中。
  • 当触发按需重新验证时,相应的缓存条目将从缓存中清除。
    • 这不同于基于时间的重新验证,后者会在获取到最新数据之前将过期数据保留在缓存中。
  • 下次发出请求时,将再次出现缓存 MISS,数据将从外部数据源获取并存储在数据缓存中。

选择退出

由于fetch请求默认不缓存,因此您不需要选择退出缓存。这意味着每次调用 fetch 时,数据都会从您的数据源获取。

注意: 数据缓存目前仅在布局、页面和路由处理程序中可用,不适用于中间件。在中间件中进行的任何获取请求默认不会被缓存。

Vercel 数据缓存

如果您的 Next.js 应用程序部署在 Vercel 上,我们建议阅读Vercel 数据缓存文档, 以更好地了解 Vercel 的特定功能。

完整路由缓存

相关术语:

您可能会看到自动静态优化静态站点生成、或静态渲染这些术语被交替使用,以指代在构建时渲染和缓存应用程序路由的过程。

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.

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

3. 客户端上的 React Hydration 和协调

在请求时,客户端:

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

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 cached

了解更多关于静态和动态渲染

持续时间

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

失效

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

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

选择退出

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

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

客户端路由器缓存

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

当用户在路由之间导航时,Next.js 会缓存已访问的路由段,并prefetches 用户可能导航到的路由。这样可以实现即时的前进/后退导航,导航之间无需完整页面重新加载,并保留 React 状态和浏览器状态。

使用路由缓存:

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

您需要知道: 此缓存专门适用于 Next.js 和服务器组件,与浏览器的bfcache不同,尽管它有类似的效果。

持续时间

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

  • Session: 缓存在导航过程中持续存在。然而,在页面刷新时会被清除。
  • 自动失效期: 布局和加载状态的缓存会在特定时间后自动失效。持续时间取决于资源是如何预取的, 以及资源是否是静态生成的:
    • 默认预取 (prefetch={null} or unspecified): 动态页面不缓存,静态页面缓存 5 分钟。
    • 完整预取 (prefetch={true} or router.prefetch): 静态和动态页面均为 5 分钟。

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

您需要知道: 实验性的staleTimes配置选项可用于调整上述自动失效时间。

失效

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

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

选择退出

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

您需要知道: 您还可以通过将<Link>组件的prefetch属性设置为false来退出prefetching

缓存交互

在配置不同的缓存机制时,了解它们之间的交互非常重要:

数据缓存和完整路由缓存

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

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

  • 要立即使数据缓存和路由器缓存失效,您可以在Server Action中使用revalidatePathrevalidateTag
  • 路由处理程序中重新验证数据缓存不会 立即使路由器缓存失效,因为路由处理程序不绑定到特定路由。这意味着路由器缓存将继续提供之前的负载,直到进行硬刷新或自动失效期结束。

APIs

下表概述了不同 Next.js API 如何影响缓存:

APIRouter CacheFull Route CacheData CacheReact Cache
<Link prefetch>Cache
router.prefetchCache
router.refreshRevalidate
fetchCacheCache
fetch options.cacheCache or Opt out
fetch options.next.revalidateRevalidateRevalidate
fetch options.next.tagsCacheCache
revalidateTagRevalidate (Server Action)RevalidateRevalidate
revalidatePathRevalidate (Server Action)RevalidateRevalidate
const revalidateRevalidate or Opt outRevalidate or Opt out
const dynamicCache or Opt outCache or Opt out
cookiesRevalidate (Server Action)Opt out
headers, searchParamsOpt out
generateStaticParamsCache
React.cacheCache
unstable_cacheCache

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

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

了解更多关于<Link>组件

router.prefetch

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

参阅useRouter hook API reference。

router.refresh

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

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

参阅 useRouter hook API reference.

fetch

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

// Cached by default. `no-store` is the default option and can be omitted.
fetch(`https://...`, { cache: "no-store" });

由于渲染输出依赖于数据,使用cache: 'no-store'也会跳过使用fetch请求的路由的完整路由缓存。也就是说,该路由将在每次请求时动态渲染,但您仍然可以在同一路由中拥有其他缓存的数据请求。

参阅fetch API Reference了解更多选项。

fetch options.cache

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

// Opt out of 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 prop 依赖于运行时传入的请求信息。使用它们将使路由退出完整路由缓存,换句话说,该路由将被动态渲染。

cookies

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

参阅 cookies API reference.

段配置选项

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

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

  • const dynamic = 'force-dynamic'
  • const revalidate = 0

参阅路由段配置文档获得更多选项。

generateStaticParams

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

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

export async function generateStaticParams() {
const posts = await fetch("https://.../posts").then((res) => res.json());

return posts.map((post) => ({
slug: post.slug,
}));
}

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

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':

export async function generateStaticParams() {
return [];
}

您需要知道: 您必须从generateStaticParams 返回一个数组,即使它是空的。否则,该路由将被动态渲染。

export const dynamic = "force-static";

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

React cache 函数

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

由于 fetch请求会自动进行记忆化,您不需要将其包裹在 React 的 cache 中然而,您可以使用cache手动记忆化数据请求,以应对fetch API 不适用的情况。例如,一些数据库客户端、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;
});