缓存
Next.js 通过缓存渲染工作和数据请求,提高了应用程序的性能并降低了成本。 本页面深入探讨了 Next.js 缓存机制、可用于配置它们的 API 以及它们之间的交互。
本页面帮助你了解 Next.js 在内部是如何工作的,但这不是使用 Next.js 时必需的知识。 大多数 Next.js 的缓存启发法则是通过你的 API 使用决定的, 并且对于最佳性能通常具有零或最小配置的默认值。
概述
以下是不同缓存机制及其目的的高级概述:
机制 | 作用 | 位置 | 目的 | 持续时间 |
---|---|---|---|---|
请求记忆 | 函数返回值 | 服务器 | 在 React 组件树中重用数据 | 每个请求生命周期 |
数据缓存 | 数据 | 服务器 | 在用户请求和部署之间存储数据 | 持久的(可重新验证) |
完整路由缓存 | HTML 和 RSC 负载 | 服务器 | 减少渲染成本,提高性能 | 持久的(可重新验证) |
路由缓存 | RSC 负载 | 客户端 | 在导航时减少服务器请求 | 用户会话或基于时间 |
默认情况下,Next.js 会尽可能多地使用缓存以提高性能并降低成本。 这意味着路由是静态渲染的,数据请求被缓存,除非你选择退出。 下面的图表展示了默认的缓存行为:当在构建时静态渲染路由以及首次访问静态路由时。
缓存行为会根据路由是静态渲染还是动态渲染、数据是否被缓存或未缓存, 以及请求是初始访问还是后续导航而发生变化。 根据您的用例,您可以为单个路由和数据请求配置缓存行为。
请求记忆
React 扩展了 [fetch
API](https://nextjs.org/docs/app/building-your-application/caching#fetch), 自动**记忆**具有相同 URL 和选项的请求。 这意味着你可以在 React 组件树的多个位置调用
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
请求记忆的工作原理
- 在渲染路由时,第一次调用特定请求时,其结果将不在内存中,因此会是缓存
MISS
。 - 因此,将执行函数,从外部源获取数据,并将结果存储在内存中。
- 在同一渲染通行中,对相同请求的后续函数调用将是缓存
HIT
,并且数据将从内存中返回而无需执行函数。 - 一旦路由已渲染并且渲染通行完成,内存将被“重置”,所有请求记忆条目将被清除。
值得知道:
- 请求记忆是 React 的一个功能,而不是 Next.js 的一个功能。在这里包含它是为了展示它如何与其他缓存机制交互。
- 记忆仅适用于 fetch 请求中的
GET
方法。 - 记忆仅适用于 React 组件树,这意味着:
- 它适用于
generateMetadata
、generateStaticParams
、Layouts
、Pages
和其他 Server Components 中的fetch
请求。 - 不适用于 Route Handlers 中的
fetch
请求,因为它们不是 React 组件树的一部分。
- 它适用于
- 对于不适合使用
fetch
的情况(例如某些数据库客户端、CMS 客户端或 GraphQL 客户端),您可以使用 React 缓存函数来记忆函数。
持续时间
缓存持续到服务器请求的生命周期结束,直到 React 组件树完成渲染。
重新验证
由于记忆不在服务器请求之间共享且仅在渲染期间适用,因此无需重新验证它。
退出
要退出在 fetch
请求中的记忆,可以将 AbortController
signal
传递给请求。
const { signal } = new AbortController()
fetch(url, { signal })
数据缓存
Next.js 具有内置的 Data Cache,它在接收到的服务器请求和部署之间保留数据获取的结果。 这是可能的,因为 Next.js 扩展了本机的 fetch API, 以允许服务器上的每个请求设置自己的持久缓存语义。
在浏览器中,fetch 的 cache
选项指示请求将与浏览器的 HTTP 缓存交互,
而在 Next.js 中,cache
选项指示服务器端请求将与服务器的 Data Cache 交互。
默认情况下,使用 fetch 的数据请求会被缓存。
您可以使用 fetch
的 cache
和 next.revalidate
选项来配置缓存行为。
Data Cache 的工作原理
- 在渲染期间第一次调用
fetch
请求时,Next.js 会检查 Data Cache 是否有缓存的响应。 - 如果找到缓存的响应,则立即返回并进行记忆。
- 如果找不到缓存的响应,则将请求发送到数据源,将结果存储在 Data Cache 中,并进行记忆。
- 对于未缓存的数据(例如
{ cache: 'no-store' }
),始终从数据源获取结果,并进行记忆。 - 无论数据是否被缓存,都会对请求进行记忆,以避免在 React 渲染通行期间为相同的数据发出重复请求。
Data Cache 和请求记忆之间的差异
尽管两种缓存机制都通过重复使用缓存的数据来提高性能, 但 Data Cache 在接收请求和部署之间是持久存在的, 而记忆只在请求的生命周期内存在。
通过记忆,我们减少了在同一渲染通行中必须跨越网络边界从渲染服务器到 Data Cache 服务器(例如 CDN 或 Edge Network)或数据源(例如数据库或 CMS)进行的重复请求的数量。 而通过 Data Cache,我们减少了对原始数据源发出的请求数量。
持续时间
Data Cache 在接收请求和部署之间是持久存在的,除非重新验证或退出。
重新验证
缓存的数据可以通过两种方式进行重新验证:
- 基于时间的重新验证:在经过一定时间并进行新请求之后重新验证数据。这对于数据很少更改且新鲜度不是那么重要的数据非常有用。
- 按需重新验证:基于事件(例如表单提交)重新验证数据。按需重新验证可以使用基于标签或路径的方法一次性重新验证一组数据。当您希望尽快显示最新数据时(例如当您的 headless CMS 中的内容更新时),这非常有用。
基于时间的重新验证
要定时重新验证数据,您可以使用 fetch
的 next.revalidate
选项设置资源的缓存寿命(以秒为单位)。
// 每小时最多重新验证一次
fetch('https://...', { next: { revalidate: 3600 } })
或者,您可以使用 Route Segment Config 选项为段中的所有 fetch
请求配置缓存。
或者,用于无法使用 fetch
的情况。
基于时间的重新验证的工作原理
- 首次带有
revalidate
的fetch
请求时,将从外部数据源获取数据并将其存储在 Data Cache 中。 - 在指定的时间段内(例如 60 秒), 调用的任何请求都将返回缓存的数据。
- 在时间段之后,下一个请求仍将返回缓存的(现在是过时的)数据。
- Next.js 将在后台触发数据的重新验证。
- 一旦成功获取数据,Next.js 将使用新数据更新 Data Cache。
- 如果后台重新验证失败,则将保留先前的数据不变。
这类似于陈旧但仍重新验证的行为。
按需重新验证
数据可以按需通过路径(revalidatePath
)或缓存标签(revalidateTag
)进行重新验证。
按需重新验证的工作原理
- 首次带有重新验证的 fetch 请求时,将从外部数据源获取数据并将其存储在 Data Cache 中。
- 当触发按需重新验证时,将从缓存中清除相应的缓存条目。
- 这与基于时间的重新验证不同,后者将保留缓存中的陈旧数据,直到获取新鲜数据。
- 下一次发出请求时,它将再次是缓存 MISS,数据将从外部数据源获取并存储在 Data Cache 中。
退出
对于单个数据获取,您可以通过将 cache
选项设置为 no-store
来退出缓存。
这意味着数据将在每次调用 fetch
时都会被获取。
// 退出对单个 `fetch` 请求的缓存
fetch(`https://...`, { cache: 'no-store' })
或者,您还可以使用“路由段配置”选项来选择不缓存特定路由段。 这将影响路由段中的所有数据请求,包括第三方库。
// 选择不缓存路由段中的所有数据请求
export const dynamic = 'force-dynamic'
** Vercel 数据缓存**
如果您的 Next.js 应用程序部署到 Vercel,我们建议您阅读 Vercel 数据缓存文档, 以更好地了解 Vercel 的特定功能。
完整路由缓存
相关术语:
您可能会看到术语自动静态优化、静态站点生成或静态渲染被互换地用来指称在构建时呈现和缓存应用程序路由的过程。
Next.js 在构建时自动呈现并缓存路由。 这是一种优化,允许您提供缓存的路由而不是在每个请求上在服务器上进行渲染,从而实现更快的页面加载。
要了解完整路由缓存的工作原理,有助于查看 React 如何处理渲染以及 Next.js 如何缓存结果:
1. 服务器上的 React 渲染
在服务器上,Next.js 使用 React 的 API 来协调渲染。 渲染工作分为多个块:按独立路由段和 Suspense 边界。
每个块分两步进行渲染:
- React 将 Server Components 渲染为一种专为流式传输优化的特殊数据格式,称为 React 服务器组件负载。
- Next.js 使用 React 服务器组件负载和客户端组件 JavaScript 指令来在服务器上渲染 HTML。
这意味着我们不必等待所有内容渲染完毕才缓存工作或发送响应。相反,我们可以在完成工作时流式传输响应。
什么是 React 服务器组件负载?
React 服务器组件负载是呈现的 React 服务器组件树的紧凑二进制表示。 它由 React 在客户端上使用,以更新浏览器的 DOM。React 服务器组件负载包含:
- Server Components 的呈现结果
- 用于渲染 Client Components 以及引用其 JavaScript 文件的占位符
- 从 Server Component 传递给 Client Component 的任何 props
要了解更多,请参阅 Server Components 文档。
2. 服务器上的 Next.js 缓存( 完整路由缓存)
完整路由缓存的默认行为,显示了在服务器上缓存静态渲染路由的 React 服务器组件负载和 HTML。 Next.js 的默认行为是在服务器上缓存路由的呈现结果(React 服务器组件负载和 HTML)。这适用于构建时静态渲染的路由,或者在重新验证时。
3. 客户端上的 React 水合和协调
在客户端上,请求时:
- 使用 HTML 立即显示客户端和服务器组件的快速非交互初始预览。
- 使用 React 服务器组件负载来协调客户端和呈现的服务器组件树,并更新 DOM。
- 使用 JavaScript 指令来水合 Client Components 并使应用程序具有交互性。
4. 客户端上的 Next.js 缓存(路由缓存)
React 服务器组件负载存储在客户端侧的路由缓存中 - 一个独立的内存中缓存,按独立路由段拆分。 这个路由缓存用于通过存储先前访问的路由和预取未来路由来改善导航体验。
5. 后续导航
在后续导航或预取期间,Next.js 将检查 React 服务器组件负载是否存储在路由缓存中。 如果是,则它将跳过向服务器发送新请求。
如果路由段不在缓存中,Next.js 将从服务器获取 React 服务器组件负载,并在客户端中填充路由缓存。
静态和动态渲染
路由在构建时是否被缓存取决于它是静态渲染还是动态渲染。 默认情况下,静态路由被缓存,而动态路由在请求时呈现,不被缓存。
此图显示了静态和动态渲染如何影响完整路由缓存,包括缓存和未缓存的数据:
了解更多关于静态和动态渲染。
持续时间
默认情况下,完整路由缓存是持久的。这意味着呈现输出在用户请求之间被缓存。
失效
有两种方式可以使完整路由缓存失效:
- 重新验证数据:重新验证 Data Cache,将通过在服务器上重新渲染组件并缓存新的渲染输出来使 Router Cache 失效。
- 重新部署:与 Data Cache 不同,它跨多个部署保持不变,完整路由缓存在新的部署上被清除。
退出
您可以退出完整路由缓存,换句话说,为每个传入的请求动态呈现组件,方法有:
- 使用动态函数:这将使路由退出完整路由缓存,并在请求时动态呈现。Data Cache 仍然可以使用。
- 使用
dynamic='force-dynamic'
或revalidate=0
路由段配置选项:这将跳过完整路由缓存和 Data Cache。这意味着组件将在每次传入请求到服务器时呈现和获取数据。Router Cache 仍然适用,因为它是客户端缓存。 - 退出 Data Cache:如果一个路由有一个未缓存的 fetch 请求,这将使路由退出完整路由缓存。特定 fetch 请求的数据将在每次传入请求时获取。其他没有退出缓存的 fetch 请求仍然会在 Data Cache 中被缓存。这允许缓存和未缓存数据的混合使用。
路由缓存
相关术语:
您可能会看到 Router Cache 被称为客户端缓存或预取缓存。 虽然预取缓存是指已预取的路由段,但客户端缓存指的是整个 Router 缓存, 其中包括访问过的和预取的段。 此缓存专门适用于 Next.js 和 Server Components,并且与浏览器的 bfcache 不同,尽管其结果类似。
Next.js 具有一个内存中的客户端缓存,用于按独立路由段拆分的 React 服务器组件负载, 持续用户会话的时间称为路由缓存。
路由缓存的工作原理
当用户在路由之间导航时,Next.js 缓存访问的路由段并预取用户可能导航到的路由(基于视口中的 <Link>
组件)。
这导致用户体验的改善:
- 由于访问的路由被缓存,所以可以进行即时的后退/前进导航,由于预取和部分渲染,导航到新路由变得更快。
- 在导航之间没有完整页面重新加载,并保留 React 状态和浏览器状态。
Router 缓存和完整路由缓存之间的差异:
Router 缓存在浏览器的临时内存中存储 React 服务器组件负载,持续用户会话。与此不同,完整路由缓存会在服务器上跨多个用户请求上持久存储 React 服务器组件负载和 HTML。
虽然完整路由缓存仅缓存在构建时静态呈现的路由,但 Router 缓存适用于静态和动态渲染的路由。
持续时间
缓存存储在浏览器的临时内存中。两个因素决定路由缓存的持续时间:
- 会话:缓存在导航中持续存在。但是,刷新页面将清除缓存。
- 自动失效周期:个别段的缓存在特定时间后会自动失效。持续时间取决于路由是静态呈现还是动态呈现:
- 动态渲染:30 秒
- 静态渲染:5 分钟
虽然页面刷新会清除所有缓存的段,但自动失效周期仅影响自上次访问或创建时的个别段。
通过将 prefetch={true}
添加到动态呈现的路由,或通过调用 router.prefetch
,
可以选择在 5 分钟内对缓存进行缓存。
失效
有两种方式可以使路由缓存失效:
- 在服务器动作中:
- 通过路径按需重新验证数据,使用 (
revalidatePath
) 或通过缓存标签使用 (revalidateTag
) - 使用
cookies.set
或cookies.delete
会使Router
缓存失效,以防止使用cookies
的路由变得陈旧(例如身份验证)。
- 通过路径按需重新验证数据,使用 (
- 调用
router.refresh
将使Router
缓存失效,并为当前路由向服务器发出新请求。
退出
无法退出路由缓存。
您可以通过将 <Link>
组件的 prefetch
属性设置为 false
来退出预取,
但这仍然会暂时存储路由段,以允许在嵌套段(例如选项卡栏)之间进行即时导航,
或在前进和后退导航之间进行导航。仍然会缓存访问过的路由。
缓存交互
在配置不同的缓存机制时,了解它们如何相互影响至关重要:
数据缓存和完整路由缓存
- 重新验证或退出数据缓存将使完整路由缓存失效,因为呈现输出依赖于数据。
- 失效或退出完整路由缓存不会影响数据缓存。您可以动态呈现一个既有缓存又有未缓存数据的路由。 当页面的大部分使用缓存数据时,但有一些组件依赖于需要在请求时获取的数据时,这非常有用。 您可以动态呈现,而无需担心重新获取所有数据的性能影响。
数据缓存和客户端路由缓存
- 在路由处理程序 中重新验证数据缓存不会立即使路由缓存失效,因为路由处理程序与特定路由无关。 这意味着路由缓存将继续提供先前的有效负载,直到进行强制刷新或自动失效周期结束。
- 要立即使数据缓存和路由缓存失效,您可以在
服务器动作
中使用
revalidatePath
或revalidateTag
。
API
以下表格概述了不同的 Next.js API 如何影响缓存:
API | Router Cache | Full Route Cache | Data Cache | React Cache |
---|---|---|---|---|
<Link prefetch> | Cache | |||
router.prefetch | Cache | |||
router.refresh | Revalidate | |||
fetch | Cache | Cache | ||
fetch options.cache | Cache or Opt out | |||
fetch options.next.revalidate | Revalidate | Revalidate | ||
fetch options.next.tags | Cache | Cache | ||
revalidateTag | Revalidate (Server Action) | Revalidate | Revalidate | |
revalidatePath | Revalidate (Server Action) | Revalidate | Revalidate | |
const revalidate | Revalidate or Opt out | Revalidate or Opt out | ||
const dynamic | Cache or Opt out | Cache or Opt out | ||
cookies | Revalidate (Server Action) | Opt out | ||
headers , useSearchParams , searchParams | Opt out | |||
generateStaticParams | ||||
React.cache | Cache | |||
unstable_cache |
<Link>
默认情况下,<Link>
组件会自动从完整路由缓存中预取路由,并将 React 服务器组件有效负载添加到路由缓存中。
要禁用预取,可以将 prefetch 属性设置为 false。但这不会永久跳过缓存,当用户访问路由时,该路由段仍然会在客户端缓存。
了解更多关于 <Link>
组件的信息。
router.prefetch
useRouter
钩子的 prefetch
选项可用于手动预取路由。
这会将 React 服务器组件有效负载添加到路由缓存中。
查看 useRouter
钩子的 API 参考。
router.refresh
useRouter
钩子的 refresh
选项可用于手动刷新路由。这将完全清除路由缓存,并向服务器请求当前路由的新数据。refresh 不会影响数据缓存或完整路由缓存。
呈现结果将在客户端进行对比,同时保留 React 状态和浏览器状态。
查看 useRouter
钩子的 API 参考。
fetch
从 fetch
返回的数据会自动缓存在数据缓存中。
// 默认缓存。'force-cache' 是默认选项,可以省略。
fetch(`https://...`, { cache: 'force-cache' })
查看 fetch
API 参考以获取更多选项。
fetch options.cache
您可以通过将 cache
选项设置为 no-store
来退出对个别数据fetch
请求的缓存:
// 退出缓存
fetch(`https://...`, { cache: 'no-store' })
由于呈现输出依赖于数据,使用 cache: 'no-store'
也会跳过使用 fetch
请求的路由的完整路由缓存。
也就是说,该路由将在每次请求时动态呈现,但您仍然可以在同一路由中拥有其他已缓存的数据请求。
查看 fetch
API 参考以获取更多选项。
fetch options.next.revalidate
您可以使用 fetch
的 next.revalidate
选项来设置个别 fetch
请求的重新验证周期(以秒为单位)。
这将重新验证数据缓存,进而重新验证完整路由缓存。将获取新数据,并在服务器上重新渲染组件。
// 最长每小时重新验证一次
fetch(`https://...`, { next: { revalidate: 3600 } })
查看 fetch
API 参考以获取更多选项。
fetch options.next.tags
和 revalidateTag
Next.js 具有用于细粒度数据缓存和重新验证的缓存标记系统。
- 使用
fetch
或unstable_cache
时,您可以选择使用一个或多个标记来标记缓存条目。 - 然后,您可以调用
revalidateTag
来清除与该标记关联的缓存条目。
例如,当获取数据时,可以设置标记:
// 使用标记缓存数据
fetch(`https://...`, { next: { tags: ['a', 'b', 'c'] } })
然后,使用标记调用 revalidateTag
来清除缓存条目:
// 重新验证具有特定标记的条目
revalidateTag('a')
根据您要实现的目标,可以在两个位置使用 revalidateTag
:
- Route Handlers - 以响应第三方事件(例如 Webhook)重新验证数据。这不会立即使路由缓存失效,因为路由处理程序与特定路由无关。
- Server Actions - 在用户操作后(例如表单提交)重新验证数据。这将使关联路由的路由缓存失效。
revalidatePath
revalidatePath
允许您手动重新验证数据,并在单个 操作中重新渲染特定路径下的路由段。
调用 revalidatePath
方法将重新验证数据缓存,进而使完整路由缓存失效。
revalidatePath('/')
根据您要实现的目标,可以在两个位置使用 revalidatePath
:
- Route Handlers - 以响应第三方事件(例如 Webhook)重新验证数据。
- Server Actions - 在用户交互后(例如表单提交、点击按钮)重新验证数据。
有关更多信息,请查看 revalidatePath
API 参考。
revalidatePath
与 router.refresh
的区别:
调用 router.refresh
将清除 Router 缓存,并在服务器上重新呈现路由段,
而不会使数据缓存或完整路由缓存失效。
区别在于 revalidatePath
清除数据缓存和完整路由缓存,
而 router.refresh()
不会更改数据缓存和完整路由缓存,因为它是客户端 API。
动态函数
cookies
、headers
、useSearchParams
和 searchParams
都是依赖于运行时传入请求信息的动态函数。使用它们将使路由退出完整路由缓存,换句话说,该路由将在每次传入请求时动态呈现。
cookies
在 Server Action 中使用 cookies.set
或 cookies.delete
会使路由缓存失效,
以防止使用 cookies 的路由变得陈旧(例如反映身份验证更改)。
请查看 cookies
API 参考。
路由段配置选项
路由段配置选项可用于覆盖路由段的默认值或在无法使用 fetch API(例如数据库客户端或第三方库)时使用。
以下路由段配置选项将退出数据缓存和完整路由缓存:
const dynamic = 'force-dynamic'
const revalidate = 0
有关更多选项,请查看路由段配置文档。
generateStaticParams
对于动态段
(例如 app/blog/[slug]/page.js
),
在构建时由 generateStaticParams
提供的路径将缓存在完整路由缓存中。
在请求时,Next.js 还将缓存那些在构建时未知的路径,第一次访问时。
您可以通过在路由段中使用 export const dynamicParams = false
选项来禁用请求时的缓存。
使用此配置选项时,只会提供由 generateStaticParams
提供的路径,
并且其他路由将返回 404 或匹配(对于捕获所有路由)。
查看 generateStaticParams
API 参考。
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
})