Caching in 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 函数,但只执行一次。
例如,如果你需要在整个路由中使用相同的数据(例如在 Layout、Page 和多个组件中),你不必在树的顶部获取数据,然后在组件之间传递 props。相反,你可以在需要数据的组件中直接获取数据,而不必担心为同一数据在网络上发出多个请求的性能影响。
async function getItem() {
// `fetch` 函数会自动记忆化并缓存结果
const res = await fetch('https://.../item/1')
return res.json()
}
// 此函数被调用两次,但只在第一次执行
const item = await getItem() // 缓存未命中
// 第二次调用可以在路由的任何位置
const item = await getItem() // 缓存命中
请求记忆化的工作原理
- 在渲染路由过程中,当特定请求第一次被调用时,其结果不会在内存中,这将是缓存
未命中
。 - 因此,该函数将被执行,数据将从外部源获取,结果将存储在内存中。
- 同一渲染过程中对请求的后续函数调用将是缓存
命中
,数据将从内存中返回,而不会执行函数。 - 一旦路由被渲染完成,渲染过程结束,内存被"重置",所有请求记忆化条目被清除。
值得注意的是:
- 请求记忆化是 React 特性,而非 Next.js 特性。这里提到它是为了展示它如何与其他缓存机制交互。
- 记忆化仅适用于
fetch
请求中的GET
方法。- 记忆化仅适用于 React 组件树,这意味着:
- 它适用于
generateMetadata
、generateStaticParams
、Layouts、Pages 和其他服务器组件中的fetch
请求。- 它不适用于路由处理程序中的
fetch
请求,因为它们不是 React 组件树的一部分。- 对于
fetch
不适用的情况(例如某些数据库客户端、CMS 客户端或 GraphQL 客户端),你可以使用 Reactcache
函数 来记忆化函数。
持续时间
缓存持续时间为服务器请求的生命周期,直到 React 组件树完成渲染。
重新验证
由于记忆化不在服务器请求之间共享,且仅在渲染期间适用,因此无需重新验证它。
退出
记忆化仅适用于 fetch
请求中的 GET
方法,其他方法如 POST
和 DELETE
不会被记忆化。这种默认行为是 React 的优化,我们不建议退出。
要管理单个请求,你可以使用 signal
属性和 AbortController
。但这不会让请求退出记忆化,而是中止正在进行的请求。
const { signal } = new AbortController()
fetch(url, { signal })
数据缓存
Next.js 有一个内置的数据缓存,它可以在服务器请求和部署之间持久化数据获取的结果。这是可能的,因为 Next.js 扩展了原生 fetch
API,允许服务器上的每个请求设置自己的持久缓存语义。
值得注意的是:在浏览器中,
fetch
的cache
选项表示请求将如何与浏览器的 HTTP 缓存交互,而在 Next.js 中,cache
选项表示服务器端请求将如何与服务器的数据缓存交互。
你可以使用 fetch
的 cache
和 next.revalidate
选项来配置缓存行为。
数据缓存的工作原理
- 在渲染过程中首次调用带有
'force-cache'
选项的fetch
请求时,Next.js 会检查数据缓存中是否有缓存的响应。 - 如果找到缓存的响应,它会立即返回并被记忆化。
- 如果未找到缓存的响应,则会向数据源发出请求,结果会存储在数据缓存中并被记忆化。
- 对于未缓存的数据(例如未定义
cache
选项或使用{ cache: 'no-store' }
),结果始终从数据源获取,并被记忆化。 - 无论数据是否被缓存,请求都会被记忆化,以避免在 React 渲染过程中为相同的数据发出重复请求。
数据缓存与请求记忆化的区别
虽然这两种缓存机制都通过重用缓存数据来提高性能,但数据缓存在传入请求和部署之间是持久的,而记忆化只在请求的生命周期内持续。
持续时间
数据缓存在传入请求和部署之间是持久的,除非你重新验证或选择退出。
重新验证
缓存的数据可以通过两种方式重新验证:
- 基于时间的重新验证:在经过一定时间并发出新请求后重新验证数据。这适用于不经常变化且鲜度不太重要的数据。
- 按需重新验证:基于事件(例如表单提交)重新验证数据。按需重新验证可以使用基于标签或基于路径的方法一次性重新验证一组数据。当你希望确保尽快显示最新数据时(例如当你的无头 CMS 中的内容更新时),这很有用。
基于时间的重新验证
要按时间间隔重新验证数据,你可以使用 fetch
的 next.revalidate
选项来设置资源的缓存生命周期(以秒为单位)。
// 最多每小时重新验证一次
fetch('https://...', { next: { revalidate: 3600 } })
或者,你可以使用路由段配置选项来配置段中的所有 fetch
请求,或者在无法使用 fetch
的情况下使用。
基于时间的重新验证的工作原理
- 当带有
revalidate
的 fetch 请求第一次被调用时,数据将从外部数据源获取并存储在数据缓存中。 - 在指定的时间范围内(例如 60 秒)发出的任何请求都将返回缓存的数据。
- 超过时间范围后,下一个请求仍将返回缓存的(现在已过时的)数据。
- Next.js 将在后台触发数据的重新验证。
- 一旦数据成功获取,Next.js 将用新鲜数据更新数据缓存。
- 如果后台重新验证失败,之前的数据将保持不变。
这类似于stale-while-revalidate行为。
按需重新验证
数据可以通过路径 (revalidatePath
) 或缓存标签 (revalidateTag
) 按需重新验证。
按需重新验证的工作原理
- 当
fetch
请求第一次被调用时,数据将从外部数据源获取并存储在数据缓存中。 - 当触发按需重新验证时,相应的缓存条目将从缓存中清除。
- 这与基于时间的重新验证不同,后者会在获取新鲜数据前保留缓存中的过时数据。
- 下次发出请求时,将再次缓存
未命中
,数据将从外部数据源获取并存储在数据缓存中。
退出
如果你_不_想缓存 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 边界。
每个块都分两步渲染:
- 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 服务器组件负载,并在客户端填充路由器缓存。
静态和动态渲染
路由是否在构建时缓存取决于它是静态渲染还是动态渲染。静态路由默认会被缓存,而动态路由在请求时渲染,不会被缓存。
下图显示了静态和动态渲染路由之间的区别,包含缓存和未缓存的数据:
了解更多关于静态和动态渲染的信息。
持续时间
默认情况下,完整路由缓存是持久的。这意味着渲染输出会在用户请求之间缓存。
失效
有两种方法可以使完整路由缓存失效:
退出
你可以通过以下方式选择退出完整路由缓存,或者换句话说,为每个传入请求动态渲染组件:
- 使用动态 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
配置选项可用于调整上述自动失效时间。
失效
有两种方法可以使路由器缓存失效:
- 在服务器操作中:
- 通过路径 (
revalidatePath
) 或缓存标签 (revalidateTag
) 按需重新验证数据 - 使用
cookies.set
或cookies.delete
会使路由器缓存失效,以防止使用 cookies 的路由变得过时(例如身份验证)。
- 通过路径 (
- 调用
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
钩子的 prefetch
选项可用于手动预取路由。这会将 React 服务器组件负载添加到路由器缓存中。
参见 useRouter
钩子 API 参考。
router.refresh
useRouter
钩子的 refresh
选项可用于手动刷新路由。这会完全清除路由器缓存,并为当前路由向服务器发出新请求。refresh
不影响数据或完整路由缓存。
渲染结果将在客户端进行协调,同时保留 React 状态和浏览器状态。
参见 useRouter
钩子 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
你可以使用 fetch
的 next.revalidate
选项来设置单个 fetch
请求的重新验证期(以秒为单位)。这将重新验证数据缓存,从而重新验证完整路由缓存。新数据将被获取,组件将在服务器上重新渲染。
// 最多在 1 小时后重新验证
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
,取决于你想要实现什么:
- 路由处理程序 - 响应第三方事件(例如 webhook)重新验证数据。这不会立即使路由器缓存失效,因为路由处理程序不与特定路由相关联。
- 服务器操作 - 在用户操作后重新验证数据(例如表单提交)。这将使关联路由的路由器缓存失效。
revalidatePath
revalidatePath
允许你手动重新验证数据并在单个操作中重新渲染特定路径下的路由段。调用 revalidatePath
方法会重新验证数据缓存,从而使完整路由缓存失效。
revalidatePath('/')
你可以在两个地方使用 revalidatePath
,取决于你想要实现什么:
查看 revalidatePath
API 参考了解更多信息。
revalidatePath
与router.refresh
的区别:调用
router.refresh
将清除路由器缓存,并在服务器上重新渲染路由段,而不会使数据缓存或完整路由缓存失效。区别在于
revalidatePath
清除数据缓存和完整路由缓存,而router.refresh()
不会改变数据缓存和完整路由缓存,因为它是客户端 API。
动态 API
像 cookies
和 headers
这样的动态 API,以及 Pages 中的 searchParams
属性都依赖于运行时传入的请求信息。使用它们将使路由退出完整路由缓存,换句话说,路由将被动态渲染。
cookies
在服务器操作中使用 cookies.set
或 cookies.delete
会使路由器缓存失效,以防止使用 cookies 的路由变得过时(例如反映身份验证变化)。
参见 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
提供完整的路径列表:
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())
// 在构建时渲染前 10 篇文章
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 或匹配(在全捕获路由的情况下)。
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
})