1. 引言:为何 Next.js 中的缓存如此重要?
Next.js 通过其精密的缓存系统,极大地提升了应用程序的性能、降低了服务器负载,并通过提供预先计算的结果或数据来最小化运营成本。对于任何期望构建优化应用的开发者而言,深入理解这些缓存层是至关重要的。在技术面试中,对 Next.js 缓存机制的掌握程度,往往能体现出开发者对 Next.js 核心原理的理解深度,以及构建可扩展、高性能 Web 应用的能力。因此,清晰阐述这些概念,将是面试中的一大亮点。
缓存并非 Next.js 中的一个孤立特性,而是其架构设计的基石,尤其是在 App Router 模式下。框架内置了多层次、多样化的缓存策略,从服务器端的数据获取到客户端的导航体验,均有覆盖。这表明 Next.js 的设计目标是全面控制和优化整个请求-响应生命周期及后续的用户交互,而非仅仅关注局部优化。如果开发者未能充分理解缓存机制,不仅难以完全发挥 Next.js 的性能优势,在排查性能瓶颈时也会遇到障碍。因此,面试官通常会通过缓存相关问题来考察候选人对 Next.js 底层运作方式的认知。
2. 能否简要介绍一下 Next.js 中的不同缓存机制?
Next.js 采用了一种多层次的缓存策略,以应对不同场景下的性能优化需求。从高层次来看,这些机制主要包括
- 请求记忆化 (Request Memoization):在单次服务器端渲染过程中,通过缓存具有相同 URL 和选项的 fetch 请求,优化数据获取。
- 数据缓存 (Data Cache):持久化 fetch 请求的结果,使其能够跨越多个服务器请求乃至多次部署,充当服务器端的数据存储。
- 全路由缓存 (Full Route Cache):在服务器端缓存静态渲染或经过重新验证的路由的 HTML 和 React 服务器组件负载 (RSC Payload),从而加速响应。
- 客户端路由器缓存 (Client-side Router Cache):浏览器中的一个内存缓存,用于存储已访问路由的 RSC Payload,实现更快的客户端导航。
这些缓存层共同构成了一个从服务器数据获取到客户端导航优化的完整解决方案。这种全面的设计体现了 Next.js 致力于提升整体应用性能的决心。
为了更清晰地对比这些缓存机制,下表总结了它们的核心特性:
Next.js 缓存层概览
缓存类型 | 缓存内容 | 位置 | 默认持续时间 | 主要失效触发器 |
---|---|---|---|---|
请求记忆化 | fetch 请求的结果 (内存中) | 服务器端 | 单个服务器请求的生命周期 | 请求结束自动清除 |
数据缓存 | fetch 请求的结果 (持久化存储) | 服务器端 | 持久 (除非重新验证或选择退出) | 基于时间的重新验证、按需重新验证 (revalidatePath, revalidateTag) |
全路由缓存 | 路由的 HTML 和 RSC Payload | 服务器端 | 持久 (静态路由),重新部署时清除 | 数据缓存重新验证、重新部署 |
客户端路由器缓存 | 路由段的 RSC Payload (内存中) | 客户端 | 用户会话期间;根据静态/动态及预取状态有自动失效期 (例如30秒或5分钟) | router.refresh()、Server Action 中的 revalidatePath/revalidateTag、Cookie 修改、页面刷新 |
3. Next.js 中的请求记忆化是什么?它如何工作,生命周期是怎样的?
请求记忆化是 Next.js 对原生 fetch API 的一项扩展,旨在自动记忆化在同一次服务器端渲染过程中具有相同 URL 和选项的请求。
- 位置:此机制作用于服务器端,具体来说,是在为单个请求渲染 React 组件树的过程中。
- 目的:核心目标是避免在单个渲染生命周期内对同一数据进行冗余的 fetch 调用。例如,当多个组件(可能处于组件树的不同层级)都需要请求相同的资源时,请求记忆化可以确保实际的网络请求只发生一次。
- 生命周期/持续时间:请求记忆化的生命周期与单个服务器请求-响应周期紧密绑定。一旦该请求的 React 组件树完成渲染,所有记忆化条目都会被清除。这意味着它是一种请求内 (intra-request) 的优化,而非跨请求 (inter-request) 的缓存。
- 工作原理:
- 当在渲染路由时首次调用特定的 fetch(url, options) 时,其结果在内存中不存在,这被视为一次缓存 MISS。此时,函数会实际执行,数据将从外部源获取,并且结果会被存储在内存中。
- 在同一个渲染过程中,如果后续有使用完全相同的 url 和 options 的 fetch 调用,这将是一次缓存 HIT。数据会直接从内存中返回,而不会再次执行函数或发起网络请求。
- 重新验证:由于记忆化数据不跨服务器请求共享,并且仅在渲染期间应用,因此它没有传统意义上的“重新验证”机制。数据在请求结束后即被清除,无需手动干预。
- 选择退出:请求记忆化主要适用于 GET 方法的 fetch 请求。其他 HTTP 方法(如 POST、DELETE 等)不会被记忆化。通常不建议选择退出 GET 请求的记忆化行为,因为它是一项有益的默认优化。虽然可以使用 AbortController 的 signal 属性来管理单个请求,但这并不会选择退出记忆化本身。
- 交互:请求记忆化适用于 generateMetadata、generateStaticParams、布局 (Layouts)、页面 (Pages) 以及其他服务器组件 (Server Components) 中的 fetch 请求。然而,它不适用于路由处理器 (Route Handlers),因为路由处理器不属于 React 组件渲染树的一部分。
理解请求记忆化的一个关键点在于其作用域。它优化的是单次服务器渲染内部的数据获取,例如,一个父组件和一个子组件在同一次页面加载过程中都尝试获取相同的用户配置数据。它并不能帮助优化两个不同用户请求同一页面时的场景,后者则由数据缓存等其他机制处理。这种自动化的行为简化了常见场景下的开发,但开发者也需要意识到它的存在,以便准确理解数据流。例如,如果开发者期望在同一次渲染传递中某个 fetch 调用能够重新执行(比如为了测试某种回退逻辑),他们可能会感到意外,除非他们了解请求记忆化的机制。
4. 请解释 Next.js 的数据缓存。如何管理其持久性和重新验证(基于时间 vs. 按需)?
数据缓存是 Next.js 内建的一个持久化服务器端缓存层,用于存储 fetch 请求的结果,这些结果可以在传入的服务器请求之间共享,甚至可以跨越多次应用部署而保持有效。
- 位置:服务器端。
- 目的:核心目的是减少对外部数据源的冗余请求,从而降低延迟、节约 API 调用成本,并通过提供缓存数据来提升应用性能。
- 持续时间:默认情况下,数据缓存是持久的,除非被明确重新验证或通过配置选择退出缓存。这种跨部署的持久性是一个非常强大的特性,意味着即使在新的代码版本上线后,先前缓存的有效数据仍可被继续使用,从而减轻了新部署初期对数据源的冲击。
- 工作原理:
- 当一个 fetch 请求(通常是带有 cache: 'force-cache' 选项,或者在非动态请求中默认行为)被发起时,Next.js 会首先检查数据缓存中是否存在该请求的缓存响应。
- 缓存命中 (Cache HIT):如果找到了缓存响应,它会立即返回该响应,并同时将其结果进行请求记忆化,供当前渲染过程中的其他相同请求使用。
- 缓存未命中 (Cache MISS):如果未找到缓存响应,则会向实际的数据源发起请求。获取到的结果不仅会存储在数据缓存中以供后续请求使用,也会被请求记忆化,用于当前渲染过程。
- 对于未被缓存的数据(例如,没有定义 cache 选项或使用了
{ cache: 'no-store' }
),结果总是从数据源获取,但仍然会被请求记忆化,以避免在同一次 React 渲染过程中对相同数据发起重复请求。
- 重新验证机制:正确管理数据缓存的生命周期至关重要,Next.js 提供了两种主要的重新验证策略:
- 基于时间的重新验证 (Time-based Revalidation):
- 通过在 fetch 请求的 options 对象中使用 next.revalidate 属性来配置,例如
fetch(url, { next: { revalidate: 60 } })
,单位为秒。 - 这会为资源设置一个缓存生命周期。在指定的时间范围内(如60秒),任何对该资源的请求都将返回缓存数据。当时间范围过后,下一个请求到达时,它仍将首先返回缓存中(现在是陈旧的)数据。与此同时,Next.js 会在后台异步触发数据的重新验证过程。一旦数据成功获取,Next.js 将使用新的数据更新数据缓存中的条目。如果后台重新验证失败,之前缓存的陈旧数据将继续被保留和提供。这种行为被称为 stale-while-revalidate。
- "stale-while-revalidate" 行为是一种刻意的权衡,它优先保证了应用的可用性和感知性能,而不是在短时间内对数据新鲜度的严格要求。用户会立即得到响应(尽管可能是旧数据),而无需等待新数据的获取。这对于非关键性数据更新是一种常见的优化模式。
- 通过在 fetch 请求的 options 对象中使用 next.revalidate 属性来配置,例如
- 按需重新验证 (On-demand Revalidation):
- 允许开发者在特定事件发生后(例如数据更新后)以编程方式精确地使缓存失效。
- 通过路径 (revalidatePath(path)):使与特定路由路径相关联的数据缓存失效。这通常在 Server Action 或 API 路由中使用。
- 通过缓存标签 (revalidateTag(tag)):使与特定缓存标签相关联的数据缓存失效。开发者可以在 fetch 请求中通过 next.tags 选项为数据打上标签,例如
fetch(url, { next: { tags: } })
。 - 与基于时间的重新验证不同,按需重新验证会直接清除缓存中的相应条目。这意味着下一次对该数据的请求将是缓存 MISS,必须从数据源重新获取新数据并存入缓存,而不是先提供陈旧数据。
- 缓存标签 (revalidateTag) 相较于基于路径 (revalidatePath) 的重新验证,提供了一种更细粒度且解耦的缓存管理方式,尤其适用于在多个路由间共享的数据。例如,如果一个产品详情数据同时展示在产品页、特色产品列表和用户愿望清单中,通过路径更新会非常繁琐。而使用标签(如 product-123),只需在产品数据更新后重新验证该标签,所有使用了此标签的 fetch 调用(及其在数据缓存中的条目)都会失效,无论它们位于哪个页面。
- 基于时间的重新验证 (Time-based Revalidation):
- 选择退出:
- 可以通过在 fetch 请求中设置
{ cache: 'no-store' }
来让特定的响应不进入数据缓存。这意味着每次请求都会从数据源获取最新数据(但请注意,该次请求的结果在当前渲染周期内仍会被请求记忆化。
- 可以通过在 fetch 请求中设置
数据缓存是 Next.js 实现高性能动态内容服务和降低外部 API 调用频率的核心。其跨部署的持久性特性,结合灵活的重新验证策略,使得开发者能够精细地平衡数据新鲜度与应用性能。
5. 全路由缓存存储了什么?静态渲染与动态渲染如何影响它?
全路由缓存是 Next.js 在服务器端实现的一项重要优化,它存储了路由的完整渲染输出,具体包括 React 服务器组件负载 (RSC Payload) 和最终生成的 HTML。
- 位置:服务器端。
- 目的:通过直接提供预先渲染好的路由内容,而不是在每次请求时都重新执行渲染逻辑,从而显著减少渲染成本,提升应用性能,加快响应速度。
- 持续时间:对于静态渲染的路由,全路由缓存默认是持久的。然而,与数据缓存不同,全路由缓存会在应用进行新的部署时被清除。
- 工作原理(渲染流程):
- React 在服务器端渲染:Next.js 利用 React 的 API 来协调渲染过程。渲染工作被分解为多个块,通常是按单个路由段 (route segment) 和 Suspense 边界划分。每个块的渲染分两步进行:
- React 将服务器组件渲染成一种特殊的数据格式,这种格式经过优化,适合流式传输,称为 React 服务器组件负载 (RSC Payload)。
- Next.js 使用 RSC Payload 和客户端组件的 JavaScript 指令,在服务器上进一步渲染生成最终的 HTML。
- Next.js 在服务器端缓存 (全路由缓存):Next.js 的默认行为是在服务器上缓存上述渲染结果(即 RSC Payload 和 HTML)。这适用于在构建时静态渲染的路由,或在重新验证期间更新的路由。
- React 在客户端进行水合 (Hydration) 与协调:当用户请求一个路由时,在客户端:
- 服务器返回的 HTML 用于立即显示页面的快速、非交互式初始预览,这包括服务器组件和客户端组件的静态部分。
- RSC Payload 用于在客户端协调(reconcile)已渲染的服务器组件树,并更新 DOM 结构。
- JavaScript 指令则用于水合客户端组件,使其具备交互能力。
- React 在服务器端渲染:Next.js 利用 React 的 API 来协调渲染过程。渲染工作被分解为多个块,通常是按单个路由段 (route segment) 和 Suspense 边界划分。每个块的渲染分两步进行:
- 静态渲染 vs. 动态渲染:
- 静态路由 (Static Routes):在构建时(或在后续的重新验证过程中)进行渲染,其输出(HTML 和 RSC Payload)默认存储在全路由缓存中。这些路由的响应速度非常快。全路由缓存的这种机制,使得纯静态内容的路由可以获得极致的性能。
- 动态路由 (Dynamic Routes):如果路由中使用了动态函数(如 cookies()、headers()),或者其包含的 fetch 请求配置为 cache: 'no-store' 或 revalidate: 0,那么该路由将在请求时按需渲染。这类动态渲染的路由的输出不会被存储在全路由缓存中。
- 失效机制:
- 数据重新验证:这是全路由缓存更新的关键驱动因素。当数据缓存中的某个条目被重新验证(例如,通过 revalidateTag 或 fetch 中的 next.revalidate 选项),并且该数据被某个路由使用时,Next.js 会在服务器上重新渲染该路由。新生成的 RSC Payload 和 HTML 会替换掉全路由缓存中旧的条目。这种紧密的耦合关系确保了当底层数据发生变化时,用户最终看到的页面内容也能得到更新。
- 重新部署:当应用的新版本被部署时,整个全路由缓存会被清除。这意味着新部署后,所有路由(即使是静态的)在首次被请求时都需要重新渲染(或从构建产物中读取)并填充缓存。
- 选择退出(强制动态渲染):
- 在服务器组件中使用动态 API,如 cookies()、headers() 或读取 searchParams。
- 使用路由段配置选项 export const dynamic = 'force-dynamic' 或 export const revalidate = 0。这会跳过全路由缓存和数据缓存,组件将在每次服务器请求时重新渲染并获取数据。不过,客户端的路由器缓存仍然适用。
- 如果路由内的任何一个 fetch 请求选择退出了数据缓存(即使用了
{ cache: 'no-store' })
,那么整个路由将变为动态渲染,并因此选择退出全路由缓存。该特定 fetch 请求的数据会在每次请求时重新获取,而其他未选择退出缓存的 fetch 请求仍可使用数据缓存。这允许在同一路由中混合使用缓存和非缓存数据。 一个重要的影响是,即使路由中只有一小部分内容依赖于动态函数或不可缓存的数据,整个路由的输出(HTML 和 RSC Payload)都无法在全路由缓存中被有效地缓存以供后续请求使用。开发者需要意识到这种“动态性传染”效应,因为它可能因意外引入动态函数(例如,不必要地读取 cookie)而导致整个路由失去全路由缓存的性能优势。
全路由缓存的填充时机也值得注意:它不仅在构建时为真正的静态路由填充,也可以为配置了重新验证策略的路由按需填充。这使得路由不单纯是“静态”或“动态”的二元状态,而是可以存在于一个连续的光谱上,提供了类似以往 Pages Router 中 ISR (增量静态再生) 的灵活性。
6. 请描述客户端路由器缓存。它的用途、持续时间和失效方式是怎样的?
客户端路由器缓存是 Next.js 在用户浏览器中实现的一个内存缓存机制,它专门存储已访问过或已预取的路由段的 React 服务器组件负载 (RSC Payloads)。
- 位置:客户端(浏览器内存中。
- 目的:主要目的是提升客户端导航的性能和用户体验。通过缓存 RSC Payload,当用户导航到已访问或预取的路由时,应用可以几乎即时地完成页面过渡,无需为获取 RSC Payload 而发起新的服务器请求。它也为即时的前进/后退浏览器导航提供了支持。此缓存的核心价值在于优化用户在应用内部导航时的感知速度。
- 存储内容:它存储的是 RSC Payload,并且这些负载是按单个路由段(如布局 layout.js、加载状态 loading.js 和页面 page.js)进行划分和存储的。这种细粒度的存储方式使得 Next.js App Router 能够在导航时高效地更新页面部分内容,而不是进行完整的页面重载。
- 持续时间:
- 会话级别:缓存条目在同一次浏览器会话(例如,同一个标签页)内的导航中是持久的。但是,当用户执行完整的页面刷新操作时,整个客户端路由器缓存会被清除。
- 自动失效期:缓存的路由段(特别是布局和加载状态)会在特定时间后自动失效。从 Next.js 15 开始,页面段 (page.js) 默认选择退出预取。当使用 <Link prefetch={true}> (或 router.prefetch) 显式预取时,静态路由预取内容的缓存时间通常为30秒,动态路由则为5分钟。布局和加载状态通常有更长的缓存时间。
- 失效机制:
- Server Actions (服务器操作):
- 在 Server Action 中使用 revalidatePath() 或 revalidateTag() 不仅会使服务器端的数据缓存(以及间接影响的全路由缓存)失效,还会同时使客户端路由器缓存中相应的条目失效。这是确保数据在整个应用栈中保持一致性的关键机制。
- 在 Server Action 中修改 Cookies(通过 cookies.set() 或 cookies.delete())也会导致客户端路由器缓存失效。这样做是为了防止因 Cookie 值(例如,与身份验证状态相关的 Cookie)的改变而导致用户界面显示陈旧信息。 Server Actions 在此扮演了重要角色,它们提供了一条从数据变更(通常由 Server Action 触发)到客户端 UI 更新的直接路径,从而保持缓存的同步。
- router.refresh():在客户端调用 router.refresh() API 会使当前路由的客户端路由器缓存失效,并向服务器发起新的请求以获取最新的 RSC Payload。
- 页面刷新:用户执行完整的浏览器页面刷新会清除整个客户端路由器缓存。
- Server Actions (服务器操作):
- 选择退出(预取行为):
- 如前所述,从 Next.js 15 开始,页面段默认选择退出预取。
- 可以通过将 <Link> 组件的 prefetch 属性设置为 false 来禁用对特定链接目标的预取行为。
客户端路由器缓存专门存储 RSC Payload 而非完整的 HTML,这一点对于理解 App Router 的工作方式至关重要。初始页面加载时,客户端接收 HTML。对于后续的客户端导航,如果目标路由的 RSC Payload 存在于路由器缓存中,Next.js 就可以利用这个 Payload 来协调新的服务器组件树与现有的客户端组件树,并高效地更新 DOM,而无需重新请求完整的 HTML。这正是 App Router 实现平滑、快速页面转换的核心机制之一。
7. 交互与高级概念
理解各个缓存层如何独立运作固然重要,但更关键的是掌握它们之间以及与 Next.js 其他核心部分(如服务器组件、客户端组件和路由处理器)的交互方式。
- 服务器组件、客户端组件及路由处理器与缓存的交互
- 服务器组件 (Server Components):
- 可以直接使用 fetch API,这将自动与请求记忆化和数据缓存机制交互。
- 它们的渲染输出,即 RSC Payload,是构成全路由缓存和客户端路由器缓存内容的核心部分。
- 可以通过使用动态函数(如 cookies()、headers())或配置路由段的 dynamic、revalidate 选项来影响全路由缓存的行为。
- 客户端组件 (Client Components):
- 由于它们在客户端渲染,因此不直接参与服务器端的缓存(如数据缓存、全路由缓存)的构建过程。
- 它们的 JavaScript 指令是初始服务器渲染输出的一部分,用于客户端的水合过程。
- 可以通过 <Link> 组件或 router.push() 等方式触发导航,这些导航会利用客户端路由器缓存。
- 可以调用 Server Actions。这些 Server Actions 自身在服务器端执行,并能够反过来使服务器端缓存(数据缓存、全路由缓存)和客户端路由器缓存失效。
- 可以使用 router.refresh() API 来主动使当前路由的客户端路由器缓存失效。
- 路由处理器 (Route Handlers, 例如 API 路由):
- 如果在路由处理器内部使用 fetch,那么这些 fetch 调用可以与数据缓存交互(例如,缓存 GET 请求的结果)。
- 然而,请求记忆化机制不适用于路由处理器内部的 fetch 调用,因为路由处理器不属于 React 组件树的渲染生命周期。
- 路由处理器不直接产生 RSC Payload,因此它们不像页面那样直接构成全路由缓存或客户端路由器缓存的内容。但是,它们提供的数据可能被服务器组件获取,从而间接影响被缓存的内容。
- 一个重要的区别是:在路由处理器中(例如,在一个 POST 请求处理器中通过 revalidateTag)重新验证数据缓存,并不会像在 Server Action 中那样自动使客户端路由器缓存失效。这使得 Server Actions 在维护跨层缓存一致性方面更为强大。
- 服务器组件 (Server Components):
- 缓存间的联动效应:一个缓存的重新验证如何影响其他缓存?
- 数据缓存 -> 全路由缓存:这是一个至关重要的交互。当数据缓存中的某个条目(例如,通过 fetch 的 next.revalidate 选项、revalidateTag 或 revalidatePath)被重新验证,并且该数据被某个路由使用时,Next.js 会在该路由下一次被请求时(或对于基于时间的重新验证,在后台)重新渲染该路由。新生成的 RSC Payload 和 HTML 将更新全路由缓存中的对应条目。
- 数据缓存 -> 客户端路由器缓存 (通过 Server Actions):当在 Server Action 中使用 revalidateTag 或 revalidatePath 时,它不仅会使数据缓存失效(并因此可能触发全路由缓存的更新),还会通知客户端使其路由器缓存中的相关段失效。这确保了客户端在下次导航或调用 router.refresh() 后会获取最新的 RSC Payload。
- 选择退出数据缓存 -> 全路由缓存:如果一个路由内的某个 fetch 请求通过
{ cache: 'no-store' }
选择退出了数据缓存,那么整个路由将变为动态渲染,并因此选择退出全路由缓存。该路由将在每次请求时都重新渲染。 - 全路由缓存的失效不影响数据缓存:反之则不成立。使全路由缓存失效或选择退出全路由缓存(例如,通过将路由配置为动态渲染)并不会影响数据缓存。即使路由本身是动态渲染的,其中由 fetch 获取的数据仍然可以被缓存到数据缓存中。 这种失效传播存在明显的层级关系:数据缓存的变动可以传递到全路由缓存,再通过特定机制(主要是 Server Actions)传递到客户端路由器缓存。反向的自动影响通常不存在。
- 动态函数(cookies(), headers(), searchParams)对缓存的影响
- 当服务器组件使用诸如 cookies()、headers()(来自 next/headers)或 searchParams(作为页面 props 传入)等动态函数时,Next.js 会检测到这种动态性。
- 这种使用会自动使包含这些动态函数的路由选择退出全路由缓存。结果是,该路由将在请求时进行动态渲染。
- 即便如此,该路由内部的 fetch 请求仍然可以使用数据缓存,除非这些特定的 fetch 请求也明确选择了退出(例如,通过 cache: 'no-store')1。
- 此外,在 Server Action 中使用 cookies.set() 或 cookies.delete() 还会导致客户端路由器缓存失效。 这种“动态性传染”效应意味着,即使一个路由的大部分内容是静态的,只要其中一个组件使用了动态函数或依赖于不可缓存的数据获取,整个路由的输出就无法在全路由缓存层面被缓存。开发者应审慎使用动态函数,如果希望最大化利用全路由缓存,可能需要将动态部分隔离到特定的组件或子路由中,或者考虑使用客户端组件来处理高度动态和个性化的内容。
8. 配置和控制缓存行为
Next.js 提供了多种 API 和配置选项,允许开发者对不同缓存层的行为进行细致的控制。掌握这些工具是有效利用 Next.js 缓存能力的关键。
-
fetch API 选项 (对数据进行最细粒度的控制):
- cache: 'force-cache':(默认行为,除非在动态上下文中) 指示 fetch 在数据缓存中查找匹配项。如果未命中,则获取数据并存入缓存。
- cache: 'no-store':每次请求都从数据源获取数据,不存入数据缓存(但结果在当前渲染过程中仍会被请求记忆化)。此选项会导致使用该 fetch 的路由选择退出全路由缓存。
- next.revalidate: <seconds>:为数据缓存中的条目设置基于时间的重新验证策略。使用此选项的路由仍然可以是静态生成的,并利用全路由缓存,其内容会周期性地在后台更新。
- next.tags: ['tag1', 'tag2']:为 fetch 请求在数据缓存中的条目添加一个或多个标签,以便后续通过 revalidateTag() 进行按需重新验证。
-
路由段配置选项 (在路由级别进行更广泛的控制):这些选项通常在页面或布局文件中导出。
-
export const dynamic = 'auto':(默认值) Next.js 会尝试尽可能多地缓存内容,但如果检测到动态函数的使用或不可缓存的数据获取,则会自动选择动态渲染该路由段。
-
export const dynamic = 'force-dynamic':强制对该路由段进行动态渲染。这将跳过全路由缓存,并且该段内的 fetch 请求也会默认跳过数据缓存(行为类似于 revalidate: 0 且所有 fetch 都使用 cache: 'no-store')。客户端路由器缓存仍然适用。
-
export const dynamic = 'error':强制进行静态渲染。如果在此路由段中检测到动态函数或不可缓存的数据获取,则会抛出错误。
-
export const dynamic = 'force-static':强制进行静态渲染。动态函数会被视为返回空值或默认值。
-
export const revalidate = <seconds>:为该路由段设置一个重新验证间隔(影响全路由缓存以及段内 fetch 请求的数据缓存)。其行为类似于 Pages Router 中的 ISR (增量静态再生)。若设置 revalidate = 0,则等同于强制动态渲染 (dynamic = 'force-dynamic')。
-
export const fetchCache:(高级选项) 更细致地控制整个路由段内 fetch 请求的默认缓存行为,例如 'default', 'only-cache', 'force-no-store' 等。
-
-
按需重新验证函数:
- revalidateTag(tag) 和 revalidatePath(path):这两个函数用于按需触发缓存失效,通常在 Server Actions 或 API 路由处理器(例如,响应数据变更的 POST 请求)中使用。
-
客户端导航与刷新:
- <Link prefetch={boolean | undefined}>:控制客户端对链接目标进行预取的行为,这会影响客户端路由器缓存的填充。从 Next.js 15 开始,页面段默认不进行预取。
- router.refresh():客户端 API,用于使当前路由的客户端路由器缓存失效,并从服务器重新获取最新的 RSC Payload。
Next.js 的缓存配置提供了一个从非常细致(单个 fetch 调用)到较为宽泛(整个路由段)的控制范围。这种分层的方法允许开发者在路由级别应用通用的缓存策略,然后在需要时针对特定的数据获取进行微调或覆盖。理解这一点非常重要:Next.js 的默认行为(例如 dynamic = 'auto' 和 fetch 隐式使用 force-cache)是倾向于积极缓存的。因此,开发者必须主动选择退出缓存,当处理高度动态、个性化或敏感数据,以防止提供陈旧或不正确的信息时,这一点尤为关键。
为了便于记忆和查阅,下表总结了关键 API 及其对缓存的影响:
关键 API 及其对缓存的影响
API / 选项 | 路由器缓存 (Client) | 全路由缓存 (Server) | 数据缓存 (Server) | 请求记忆化 (Server, React Cache) | 简要影响描述 |
---|---|---|---|---|---|
<Link prefetch> | 缓存 | 预取路由段的 RSC Payload 并存入客户端路由器缓存。 | |||
router.prefetch | 缓存 | 以编程方式预取路由段的 RSC Payload。 | |||
router.refresh | 重新验证 | 使当前路由的客户端路由器缓存失效,并从服务器获取新数据。 | |||
fetch (默认) | 缓存 | 缓存 | 默认情况下,fetch 的结果会被数据缓存和请求记忆化。 | ||
fetch options.cache | 可能影响 | 缓存或选择退出 | 控制 fetch 是否使用数据缓存 ('force-cache', 'no-store')。'no-store' 会使路由动态化,影响全路由缓存。 | ||
fetch options.next.revalidate | 重新验证 | 重新验证 | 为数据缓存条目设置基于时间的重新验证。会触发全路由缓存的更新。 | ||
fetch options.next.tags | 缓存 (带标签) | 为数据缓存条目添加标签,用于按需重新验证。 | |||
revalidateTag | 重新验证 (Server Action) | 重新验证 | 重新验证 | 按标签按需重新验证数据缓存、全路由缓存和客户端路由器缓存 (若在 Server Action 中)。 | |
revalidatePath | 重新验证 (Server Action) | 重新验证 | 重新验证 | 按路径按需重新验证数据缓存、全路由缓存和客户端路由器缓存 (若在 Server Action 中)。 | |
const revalidate (段配置) | 重新验证或选择退出 | 重新验证或选择退出 | 设置路由段的重新验证周期,影响全路由缓存和数据缓存。0 表示动态。 | ||
const dynamic (段配置) | 缓存或选择退出 | 缓存或选择退出 | 控制路由段是静态生成、动态渲染还是报错。'force-dynamic' 会跳过全路由和数据缓存。 | ||
cookies() (在 Server Component 中) | 重新验证 (Server Action 中 set/delete 时) | 选择退出 | 读取 cookies 会使路由动态化,选择退出全路由缓存。在 Server Action 中修改 cookie 会使路由器缓存失效。 | ||
headers() (在 Server Component 中) | 选择退出 | 读取 headers 会使路由动态化,选择退出全路由缓存。 | |||
searchParams (在 Server Component 中) | 选择退出 | 读取 searchParams 会使路由动态化,选择退出全路由缓存。 | |||
generateStaticParams | 缓存 | 在构建时为动态路由段生成参数,这些路径会被全路由缓存。 | |||
React.cache | 缓存 | 记忆化函数的返回值,用于非 fetch 的数据获取,作用域为单次渲染。 | |||
unstable_cache | 缓存 | 为非 fetch 的数据获取提供持久化缓存,类似数据缓存。 |
9. 超越 fetch:React.cache 和 unstable_cache
虽然 Next.js 对 fetch API 的缓存能力进行了深度集成和优化,但在实际开发中,并非所有数据获取都通过 fetch 完成。例如,项目可能使用特定的数据库客户端、CMS SDK 或不基于 fetch 的 GraphQL 客户端。为了将这些非 fetch 的数据源也纳入 Next.js 的缓存体系,框架提供了 React.cache 和 unstable_cache 这两个工具。
- React.cache 函数:
- Next.js 中的 fetch API 会自动处理请求记忆化(通过 React 的缓存机制)和持久化缓存(通过数据缓存)。
- 然而,如果使用的是不依赖 fetch 的数据获取库,那么 fetch 自带的自动记忆化功能将无法生效。
- React.cache 是 React 提供的一个函数,它允许开发者包裹一个函数,并记忆化其返回值。通过用 React.cache 包裹那些来自其他库的数据获取调用,可以实现在单次服务器渲染传递中类似请求记忆化的效果,避免对同一非 fetch 数据源的重复调用。
- 需要明确的是,React.cache 仅提供记忆化功能(类似于 fetch 的请求记忆化),它不提供跨请求的持久化缓存,这一点与数据缓存有着本质区别 。
- unstable_cache 函数:
- 对于那些不使用 fetch 进行数据获取,并且需要实现跨请求的持久化缓存(类似于数据缓存提供的能力)的场景,Next.js 提供了 unstable_cache 函数。
- 这个函数允许缓存任何函数的执行结果,而不仅仅是 fetch 请求的结果。
- 开发者可以为其提供缓存标签和重新验证选项(如时间或按需),其功能与 fetch 结合数据缓存时的配置相似。
- unstable_cache 实际上是将数据缓存的强大能力扩展到了非 fetch 的数据源。
- 何时使用:
- 当需要在服务器的单次渲染过程中对来自非 fetch 数据源的调用进行去重时,应使用 React.cache。
- 当需要将来自非 fetch 数据源的结果进行跨多个请求和部署的持久化缓存,并且需要重新验证能力时,应使用 unstable_cache。
React.cache 和 unstable_cache 是非常重要的补充工具,它们弥合了 Next.js 内建 fetch 优化与多样化数据获取方式之间的差距。React.cache 在概念上与请求记忆化对齐,而 unstable_cache 则与数据缓存对齐,为开发者提供了在不同场景下一致的缓存思维模型。值得注意的是,unstable_cache 名称中的 unstable_ 前缀表明其 API 可能会在未来的 Next.js 版本中发生变化,开发者在使用时应关注官方文档的更新,并意识到其潜在的维护成本。
10. 总结:Next.js 缓存面试的关键要点
在准备关于 Next.js 缓存的面试时,不仅要理解每个缓存层的孤立功能,更要展现出对其整体协作方式、配置选项以及背后设计哲学的深刻认知。
核心回顾点:
- 多层系统:清晰阐述 Next.js 的四个主要缓存层:请求记忆化、数据缓存、全路由缓存和客户端路由器缓存。
- 核心属性:对每一层,都能说明其用途、位置(服务器端/客户端)、默认持续时间以及主要的失效/重新验证机制。
- fetch 的中心地位:强调 fetch API 及其选项(cache, next.revalidate, next.tags)在控制服务器端缓存(数据缓存、间接影响全路由缓存)中的核心作用。
- 渲染模式与缓存:区分静态生成(构建时缓存)、动态渲染(请求时渲染)以及各种重新验证策略(基于时间、按需)对缓存行为的影响。
- 动态性的影响:理解动态函数(如 cookies(), headers())和未缓存的数据获取(cache: 'no-store')如何导致路由选择退出全路由缓存。
- Server Actions 的角色:认识到 Server Actions 在执行数据变更和同时使多个相关缓存层(数据缓存、全路由缓存、客户端路由器缓存)失效方面的强大能力。
- 非 fetch 数据的缓存:了解何时以及如何使用 React.cache(用于单次渲染的记忆化)和 unstable_cache(用于持久化缓存)。
- 关注“为什么”:不仅要描述“是什么”和“怎么做”,更要解释“为什么”这么设计——即每个缓存机制如何为提升性能、降低成本和改善用户体验做出贡献。
常见的理解误区或面试考察点:
- 混淆请求记忆化(作用于单次请求生命周期内)与数据缓存(持久化跨请求)。
- 未能意识到单个动态函数的使用或一个 cache: 'no-store' 的 fetch 调用就可能使整个路由变为动态渲染,从而失去全路由缓存的优势。
- 忘记全路由缓存会在应用重新部署时清除,而数据缓存(如果配置得当)可以跨部署持久存在。
- 对 stale-while-revalidate(基于时间的重新验证)与按需重新验证(清除缓存,下次请求强制重新获取)的行为差异理解不清。
- 不清楚在数据发生变更后如何正确地使相关缓存失效,这可能导致用户看到陈旧的内容。
最终,其实我们希望看到的是候选人能够将这些独立的缓存知识点联系起来,形成一个关于 Next.js 如何通过缓存实现高性能应用的完整图景。能够清晰地阐述一个数据变更(例如,通过 Server Action 更新了数据库中的一条记录)如何通过一系列缓存失效和重新验证机制,最终反映到用户界面上,将充分展示候选人对 Next.js 缓存系统及其交互的深入理解。
欢迎加群