Next.js 中的缓存
Next.js 通过缓存渲染工作和数据请求来提高应用程序性能并降低成本。本页深入介绍了 Next.js 的缓存机制、可用于配置它们的 API,以及它们之间的相互作用。
提示: 本页面可以帮助你了解 Next.js 的内部工作原理,但并不是使用 Next.js 高效工作的必备知识。Next.js 的大多数缓存策略都是由你的 API 使用情况决定的,并且有默认设置以实现最佳性能,只需很少或无需配置。
概述
以下是不同缓存机制及其用途的概述:
机制 | 缓存内容 | 位置 | 目的 | 持续时间 |
---|---|---|---|---|
请求记忆化 | 函数的返回值 | 服务器 | 在 React 组件树中重用数据 | 每个请求的生命周期 |
数据缓存 | 数据 | 服务器 | 跨用户请求和部署存储数据 | 持久 (可以重新验证) |
完整路由缓存 | HTML 和 RSC 负载 | 服务器 | 减少渲染成本并提高性能 | 持久 (可以重新验证) |
路由器缓存 | RSC 负载 | 客户端 | 减少导航时的服务器请求 | 用户会话或基于时间 |
默认情况下,Next.js 会尽可能多地进行缓存以提高性能并降低成本。这意味着路由是静态渲染的,数据请求是缓存的,除非你选择退出。下图显示了默认的缓存行为:当路由在构建时静态渲染以及首次访问静态路由时。
缓存行为会根据路由是静态还是动态渲染、数据是否缓存、以及请求是初次访问还是后续导航而变化。根据你的使用场景,你可以为单个路由和数据请求配置缓存行为。
请求记忆化
Next.js 扩展了 fetch
API 以自动记忆化具有相同 URL 和选项的请求。这意味着你可以在 React 组件树的多个位置调用相同数据的 fetch 函数,但只执行一次。
例如,如果你需要在路由中使用相同的数据 (例如在布局、页面和多个组件中),你不必在树的顶部获取数据,然后在组件之间传递 props。相反,你可以在需要数据的组件中获取数据,而不必担心多次跨网络请求相同数据的性能影响。
请求记忆化的工作原理
- 在渲染路由时,第一次调用特定请求时,其结果不会在内存中,这将是缓存
未命中
。 - 因此,该函数将被执行,数据将从外部源获取,结果将存储在内存中。
- 在同一渲染过程中,后续对该请求的函数调用将是缓存
命中
,数据将从内存中返回,而不执行函数。 - 一旦路由渲染完成,渲染过程结束,内存被"重置",所有请求记忆化条目都被清除。
提示:
- 请求记忆化是 React 的功能,而不是 Next.js 的功能。它包含在这里是为了显示它如何与其他缓存机制交互。
- 记忆化仅适用于
fetch
请求中的GET
方法。- 记忆化仅适用于 React 组件树,这意味着:
- 它适用于
generateMetadata
、generateStaticParams
、布局、页面和其他服务器组件中的fetch
请求。- 它不适用于路由处理程序中的
fetch
请求,因为它们不是 React 组件树的一部分。- 对于不适合使用
fetch
的情况 (例如某些数据库客户端、CMS 客户端或 GraphQL 客户端),你可以使用 Reactcache
函数 来记忆化函数。
持续时间
缓存在服务器请求的生命周期内持续,直到 React 组件树渲染完成。
重新验证
由于记忆化不在服务器请求之间共享,并且仅在渲染期间应用,因此无需重新验证。
退出
记忆化仅适用于 fetch
请求中的 GET
方法,其他方法 (如 POST
和 DELETE
) 不会被记忆化。这是 React 的默认优化行为,我们不建议退出。
要管理单个请求,你可以使用 AbortController
的 signal
属性。但是,这不会让请求退出记忆化,而是中止正在进行的请求。
数据缓存
Next.js 有一个内置的数据缓存,可以在传入的服务器请求和部署之间持久化数据获取的结果。这是可能的,因为 Next.js 扩展了原生 fetch
API,允许服务器上的每个请求设置自己的持久缓存语义。
提示: 在浏览器中,
fetch
的cache
选项表示请求如何与浏览器的 HTTP 缓存交互,而在 Next.js 中,cache
选项表示服务器端请求如何与服务器的数据缓存交互。
默认情况下,使用 fetch
的数据请求不会被缓存。你可以使用 fetch
的 cache
和 next.revalidate
选项来配置缓存行为。
数据缓存的工作原理
- 在渲染过程中第一次调用带有
'force-cache'
选项的fetch
请求时,Next.js 会检查数据缓存中是否有缓存的响应。 - 如果找到缓存的响应,它会立即返回并被记忆化。
- 如果没有找到缓存的响应,请求会发送到数据源,结果会存储在数据缓存中并被记忆化。
- 对于未缓存的数据 (例如未定义
cache
选项或使用{ cache: 'no-store' }
),结果总是从数据源获取,并被记忆化。 - 无论数据是否缓存,请求总是被记忆化,以避免在 React 渲染过程中对相同数据进行重复请求。
数据缓存和请求记忆化之间的区别
虽然这两种缓存机制都通过重用缓存数据来提高性能,但数据缓存在传入请求和部署之间是持久的,而记忆化只在请求的生命周期内持续。
通过记忆化,我们减少了在同一渲染过程中从渲染服务器到数据缓存服务器 (例如 CDN 或边缘网络) 或数据源 (例如数据库或 CMS) 的重复请求数量。通过数据缓存,我们减少了对原始数据源的请求数量。
持续时间
数据缓存在传入请求和部署之间是持久的,除非你重新验证或选择退出。
重新验证
缓存的数据可以通过两种方式重新验证:
- 基于时间的重新验证: 在经过一定时间并发出新请求后重新验证数据。这对于不经常变化且新鲜度不是很重要的数据很有用。
- 按需重新验证: 基于事件 (例如表单提交) 重新验证数据。按需重新验证可以使用基于标签或基于路径的方法一次重新验证一组数据。当你想确保尽快显示最新数据时,这很有用 (例如当你的无头 CMS 中的内容更新时)。
基于时间的重新验证
要在定时间隔重新验证数据,你可以使用 fetch
的 next.revalidate
选项来设置资源的缓存生命周期 (以秒为单位)。
或者,你可以使用 路由段配置选项 来配置段中的所有 fetch
请求,或者在无法使用 fetch
的情况下使用。
基于时间的重新验证的工作原理
- 第一次调用带有
revalidate
的 fetch 请求时,数据将从外部数据源获取并存储在数据缓存中。 - 在指定的时间框架内 (例如 60 秒) 的任何请求都将返回缓存的数据。
- 在时间框架之后,下一个请求仍然会返回缓存的 (现在是陈旧的) 数据。
- Next.js 将在后台触发数据的重新验证。
- 一旦数据被成功获取,Next.js 将用新数据更新数据缓存。
- 如果后台重新验证失败,之前的数据将保持不变。
这类似于 stale-while-revalidate 行为。
按需重新验证
数据可以通过路径 (revalidatePath
) 或缓存标签 (revalidateTag
) 按需重新验证。
按需重新验证的工作原理
- 第一次调用
fetch
请求时,数据将从外部数据源获取并存储在数据缓存中。 - 当触发按需重新验证时,相应的缓存条目将从缓存中清除。
- 这与基于时间的重新验证不同,后者会在获取新数据之前保留陈旧数据在缓存中。
- 下次发出请求时,它将再次成为缓存
未命中
,数据将从外部数据源获取并存储在数据缓存中。
退出
由于默认情况下 fetch
请求不会被缓存,你不需要选择退出缓存。这意味着每次调用 fetch
时都会从你的数据源获取数据。
值得注意的是: 数据缓存目前仅在布局、页面和路由处理程序中可用,而不在中间件中可用。在中间件中执行的任何获取操作默认情况下都不会被缓存。
Vercel 数据缓存
如果你的 Next.js 应用程序部署在 Vercel 上,我们建议阅读 Vercel 数据缓存 文档,以更好地了解 Vercel 特定的功能。
完整路由缓存
相关术语:
你可能会看到 自动静态优化、静态站点生成 或 静态渲染 这些术语被互换使用,它们都指的是在构建时渲染和缓存应用程序路由的过程。
Next.js 在构建时自动渲染和缓存路由。这是一种优化,可以让你为每个请求提供缓存的路由,而不是在服务器上渲染,从而加快页面加载速度。
要理解完整路由缓存是如何工作的,我们需要了解 React 是如何处理渲染的,以及 Next.js 如何缓存结果:
1. React 在服务器上的渲染
在服务器上,Next.js 使用 React 的 API 来协调渲染。渲染工作被分成多个块:按个别路由段和 Suspense 边界。
每个块分两步渲染:
- React 将服务器组件渲染成一种特殊的数据格式,针对流式传输进行了优化,称为 React 服务器组件负载。
- Next.js 使用 React 服务器组件负载和客户端组件 JavaScript 指令在服务器上渲染 HTML。
这意味着我们不必等待所有内容都渲染完毕才能缓存工作或发送响应。相反,我们可以在工作完成时流式传输响应。
什么是 React 服务器组件负载?
React 服务器组件负载是渲染的 React 服务器组件树的紧凑二进制表示。它被 React 用于在客户端上更新浏览器的 DOM。React 服务器组件负载包含:
- 服务器组件的渲染结果
- 客户端组件应该渲染的位置的占位符以及它们的 JavaScript 文件的引用
- 从服务器组件传递到客户端组件的任何 props
要了解更多信息,请参阅 服务器组件 文档。
2. Next.js 在服务器上的缓存 (完整路由缓存)
Next.js 的默认行为是在服务器上缓存路由的渲染结果 (React 服务器组件负载和 HTML)。这适用于在构建时或重新验证期间静态渲染的路由。
3. React 在客户端上的水合和协调
在请求时,在客户端:
- HTML 用于立即显示客户端和服务器组件的快速非交互式初始预览。
- React 服务器组件负载用于协调客户端和渲染的服务器组件树,并更新 DOM。
- JavaScript 指令用于水合客户端组件并使应用程序具有交互性。
4. Next.js 在客户端上的缓存 (路由器缓存)
React 服务器组件负载存储在客户端路由器缓存中 - 这是一个按个别路由段分割的单独的内存缓存。这个路由器缓存用于通过存储先前访问的路由和预取未来的路由来改善导航体验。
5. 后续导航
在后续导航或预取期间,Next.js 将检查路由器缓存中是否存储了 React 服务器组件负载。如果存在,它将跳过向服务器发送新请求。
如果路由段不在缓存中,Next.js 将从服务器获取 React 服务器组件负载,并填充客户端上的路由器缓存。
静态和动态渲染
路由是否在构建时缓存取决于它是静态还是动态渲染。默认情况下,静态路由会被缓存,而动态路由则在请求时渲染,不会被缓存。
这个图表显示了静态和动态渲染的路由之间的区别,包括缓存和未缓存的数据:
了解更多关于静态和动态渲染的信息。
持续时间
默认情况下,完整路由缓存是持久的。这意味着渲染输出会在用户请求之间缓存。
失效
有两种方法可以使完整路由缓存失效:
退出
你可以通过以下方式退出完整路由缓存,或者换句话说,为每个传入请求动态渲染组件:
- 使用动态函数: 这会使路由退出完整路由缓存,并在请求时动态渲染。数据缓存仍然可以使用。
- 使用
dynamic = 'force-dynamic'
或revalidate = 0
路由段配置选项: 这将跳过完整路由缓存和数据缓存。这意味着组件将在每个传入的服务器请求时渲染,并获取数据。路由器缓存仍然适用,因为它是客户端缓存。 - 退出数据缓存: 如果一个路由有一个未缓存的
fetch
请求,这将使该路由退出完整路由缓存。特定fetch
请求的数据将在每个传入请求时获取。其他不选择退出缓存的fetch
请求仍将在数据缓存中缓存。这允许缓存数据和未缓存数据的混合。
客户端路由器缓存
Next.js 有一个内存中的客户端路由器缓存,用于存储路由段的 RSC 负载,按布局、加载状态和页面分割。
当用户在路由之间导航时,Next.js 会缓存访问过的路由段并预取用户可能导航到的路由。这导致了即时的前进/后退导航,在导航之间没有完整页面重新加载,并保留了 React 状态和浏览器状态。
使用路由器缓存:
- 布局在导航时被缓存和重用 (局部渲染)。
- 加载状态在导航时被缓存和重用,以实现即时加载状态。
- 页面默认情况下不会被缓存,但在浏览器后退和前进导航期间会被重用。你可以使用实验性的
staleTimes
配置选项来启用页面段的缓存。
提示: 这个缓存特别适用于 Next.js 和服务器组件,与浏览器的 bfcache 不同,尽管它有类似的结果。
持续时间
缓存存储在浏览器的临时内存中。有两个因素决定路由器缓存的持续时间:
- 会话: 缓存在导航过程中持续存在。但是,它会在页面刷新时被清除。
- 自动失效期: 布局和加载状态的缓存会在特定时间后自动失效。持续时间取决于资源是如何预取的:
- 默认预取 (
prefetch={null}
或未指定): 0 秒 - 完整预取: (
prefetch={true}
或router.prefetch
): 5 分钟
- 默认预取 (
虽然页面刷新会清除所有缓存的段,但自动失效期只影响从预取时间开始的单个段。
提示: 实验性的
staleTimes
配置选项可用于启用页面段的缓存。
失效
有两种方法可以使路由器缓存失效:
- 在服务器操作中:
- 通过路径 (
revalidatePath
) 或缓存标签 (revalidateTag
) 按需重新验证数据 - 使用
cookies.set
或cookies.delete
会使路由器缓存失效,以防止使用 cookie 的路由变得陈旧 (例如身份验证)。
- 通过路径 (
- 调用
router.refresh
将使路由器缓存失效,并为当前路由向服务器发出新请求。
退出
从 Next.js 15 开始,页面段默认已退出。
提示: 你还可以通过将
<Link>
组件的prefetch
属性设置为false
来选择退出预取。
缓存交互
在配置不同的缓存机制时,了解它们之间的交互很重要:
数据缓存和完整路由缓存
- 重新验证或退出数据缓存会使完整路由缓存失效,因为渲染输出依赖于数据。
- 使完整路由缓存失效或退出不会影响数据缓存。你可以动态渲染一个既有缓存数据又有未缓存数据的路由。当你的页面大部分使用缓存数据,但有几个组件依赖于需要在请求时获取的数据时,这很有用。你可以动态渲染而不必担心重新获取所有数据对性能的影响。
数据缓存和客户端路由器缓存
- 要立即使数据缓存和路由器缓存失效,你可以在服务器操作中使用
revalidatePath
或revalidateTag
。 - 在路由处理程序中重新验证数据缓存不会立即使路由器缓存失效,因为路由处理程序不与特定路由相关联。这意味着路由器缓存将继续提供先前的负载,直到进行硬刷新或自动失效期过去。
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>
默认情况下,<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
返回的数据不会自动缓存在数据缓存中。
由于渲染输出依赖于数据,使用 cache: 'no-store'
也会跳过使用 fetch
请求的路由的完整路由缓存。也就是说,该路由将在每个请求时动态渲染,但你仍然可以在同一路由中有其他缓存的数据请求。
参见 fetch
API 参考 以了解更多选项。
fetch options.cache
你可以通过将 cache
选项设置为 force-cache
来选择单个 fetch
进行缓存:
参见 fetch
API 参考 以了解更多选项。
fetch options.next.revalidate
你可以使用 fetch
的 next.revalidate
选项来设置单个 fetch
请求的重新验证期限 (以秒为单位)。这将重新验证数据缓存,进而重新验证完整路由缓存。新数据将被获取,组件将在服务器上重新渲染。
参见 fetch
API 参考 以了解更多选项。
fetch options.next.tags
和 revalidateTag
Next.js 有一个缓存标记系统,用于细粒度的数据缓存和重新验证。
- 当使用
fetch
或unstable_cache
时,你可以选择用一个或多个标记来标记缓存条目。 - 然后,你可以调用
revalidateTag
来清除与该标记关联的缓存条目。
例如,你可以在获取数据时设置一个标记:
然后,调用 revalidateTag
并带上一个标记来清除缓存条目:
你可以在两个地方使用 revalidateTag
,具体取决于你想要实现的目标:
- 路由处理程序 - 响应第三方事件 (例如 webhook) 重新验证数据。这不会立即使路由器缓存失效,因为路由处理程序不与特定路由相关联。
- 服务器操作 - 在用户操作后重新验证数据 (例如表单提交)。这将使关联路由的路由器缓存失效。
revalidatePath
revalidatePath
允许你手动重新验证数据并在一次操作中重新渲染特定路径下的路由段。调用 revalidatePath
方法会重新验证数据缓存,进而使完整路由缓存失效。
你可以在两个地方使用 revalidatePath
,具体取决于你想要实现的目标:
参见 revalidatePath
API 参考 以获取更多信息。
revalidatePath
vs.router.refresh
:调用
router.refresh
将清除路由器缓存,并在服务器上重新渲染路由段,而不会使数据缓存或完整路由缓存失效。区别在于
revalidatePath
清除数据缓存和完整路由缓存,而router.refresh()
不会改变数据缓存和完整路由缓存,因为它是一个客户端 API。
动态函数
像 cookies
和 headers
这样的动态函数,以及页面中的 searchParams
属性依赖于运行时传入的请求信息。使用它们会使路由退出完整路由缓存,换句话说,路由将被动态渲染。
cookies
在服务器操作中使用 cookies.set
或 cookies.delete
会使路由器缓存失效,以防止使用 cookie 的路由变得陈旧 (例如反映身份验证更改)。
参见 cookies
API 参考。
段配置选项
路由段配置选项可用于覆盖路由段默认值,或当你无法使用 fetch
API 时 (例如数据库客户端或第三方库)。
以下路由段配置选项将退出数据缓存和完整路由缓存:
const dynamic = 'force-dynamic'
const revalidate = 0
参见 路由段配置 文档以了解更多选项。
generateStaticParams
对于动态段 (例如 app/blog/[slug]/page.js
),generateStaticParams
提供的路径在构建时会被缓存在完整路由缓存中。在请求时,Next.js 还会缓存在构建时未知的路径,当它们首次被访问时。
要在构建时静态渲染所有路径,请向 generateStaticParams
提供完整的路径列表:
要在构建时静态渲染路径的子集,并在运行时首次访问时渲染其余路径,返回部分路径列表:
要在首次访问时静态渲染所有路径,返回一个空数组 (构建时不会渲染任何路径) 或使用 export const dynamic = 'force-static'
:
提示: 你必须从
generateStaticParams
返回一个数组,即使它是空的。否则,该路由将被动态渲染。
要在请求时禁用缓存,在路由段中添加 export const dynamicParams = false
选项。使用此配置选项时,只会提供 generateStaticParams
提供的路径,其他路由将 404 或匹配 (在捕获所有路由的情况下)。
React cache
函数
React cache
函数允许你记忆函数的返回值,使你能够多次调用同一函数,但只执行一次。
由于 fetch
请求会自动记忆化,你不需要将其包装在 React cache
中。但是,你可以使用 cache
来手动记忆化不适合使用 fetch
API 的数据请求。例如,一些数据库客户端、CMS 客户端或 GraphQL 客户端。