【Next.js】Caching

174 阅读9分钟

大部分对照文档看的,也加了一些自己的体会

请求记忆化 Request Memoization

使用 fetch API 自动 Memoization 相同 URL 和选项的请求,看下图就知道,在树的顶部获取数据,在内部组件也发出同样的请求,最终也只会发出一次。以前都是是通过 Props 传递数据 或者 Context

image.png

但是这样有限制条件

  • Request Memoization 是 React 的特性,而不是 Next.js 的特性
  • Request Memoization 仅仅使用 fetch 请求的 GET 方法,因为这类数据通常不需要改变
  • Request Memoization仅适用于 React 组件树, 这意味着
    • 它适用于generateMetadatagenerateStaticParams、Layouts、Pages 和其他服务器组件中的 fetch 请求
    • 它不适用于 Route Handlers 中的 fetch 请求,因为他们不是 React 组件树的一部分,这是因为Route Handlers 运行在服务端,并不涉及 UI 渲染,而是直接处理 HTTP 请求并返回
  • 对于不适用的情况,可以使用 React cache 函数来处理

持续时间 Duration

缓存在服务器请求的生命周期内持续,直到 React 组件树渲染完成

重新验证 Revalidating

由于 memoization 不在服务器请求之间共享,并且仅在渲染期间应用,因此无需重新验证它

退出 Opting out

memoization 仅适用于 fetch 请求中的 GET 方法,其他方法 (如 POST 和 DELETE) 不会被记忆化。这是 React 的默认优化,我们不建议退出

如果有特殊的情况,可以使用来自 AbortController 的 signal 属性。但是,这不会使请求退出 memoization,而是中止正在进行的请求

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

数据缓存 Data Cache

Next.js 有一个内置的数据缓存,可以在 server requestsdeployments 之间 persists 数据获取结果。这是可能的,因为 Next.js 扩展了原生的 fetch API,允许服务器上的每个请求设置自己的持久缓存语义

有意思的是,在浏览器中, fetch 的 cache 选项表示请求如何与浏览器的 HTTP 缓存交互,而在 Next.js 中,cache 选项表示服务器端请求如何与服务器的数据缓存交互。

我个人觉得这是因为在 Next.js 请求数据是服务器组件的行为,如果有复杂的交互除外

data cache 和 memoization 的区别

虽然这两种缓存机制都通过重用缓存数据来提高性能,但数据缓存在传入 requests 和 deployments 之间是持久的,而记忆化仅在请求的生命周期内持续

持续时间

除非你 revalidate 或者 opt-out,否则数据缓存在传入 requests 和 deployments 之间是持久的

重新验证

基于时间的重新验证

适用于不用经常变的数据

// 最多每小时重新验证一次
fetch('https://...', { next: { revalidate: 3600 } })

或者可以使用 Route Segment Config options 配置所有的 fetch请求

按需重新验证 On-demand Revalidation

数据通过 revalidatePathrevalidateTag 按需重新验证

退出 Opting out

let data = await fetch('https://api.vercel.app/blog', { cache: 'no-store' })

完整路由缓存 Full Route Cache

相关术语 Automatic Static Optimization 自动静态化 Static Site Generation 静态站点生成 Static Rendering 静态渲染

Next.js 在 build 阶段会自动渲染和缓存路由,这是一种优化手段,允许你提供缓存路由而不是每次请求时都在服务器上进行渲染,从而实现更快的页面加载

1、React在服务器上的渲染

在服务器上,Next.js 使用 React 的 API 来协调渲染。渲染工作被分成块:按个 individual routes 和 Suspense 边界。

每个块分两步渲染:

  1. React 将服务器组件渲染成一种特殊的数据格式,这种格式针对流式传输进行了优化,称为React Server Component Payload(RSC Payload)
  2. Next.js 使用 RSC 和客户端组件 JavaScript 指令在服务器上渲染 HTML

这意味着我们不必等待所有内容都渲染完成才能缓存工作或发送响应。相反,我们可以在工作完成时流式传输响应。

什么是 React Server Component Payload

RSC Payload 是已渲染的 React 服务器组件树的紧凑二进制(compact binary)表示。React 在客户端使用它来更新浏览器的 DOM。React 服务器组件负载包含:

  • 服务器组件的渲染结果
  • 客户端组件应该渲染的位置的占位符以及它们的 JavaScript 文件的引用
  • 从服务器组件传递到客户端组件的所有 props

要了解更多,请参阅服务器组件文档。

2、Next.js 在服务器的缓存

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

3、React 在客户端的 Hydration 和 Reconciliation

在请求时,在客户端:

1、HTML 用于立即显示客户端和服务器组件的快速非交互式初始预览 2、RSC Payload 用于 reconcile 客户端和一渲染的服务组件树,并且更新 DOM 3、 Javascript 指令用于 hydrate 客户端组件并且是应用程序具有交互性

4、Next.js 在可短的缓存(路由器缓存)

RSC Payload 存储在客户端 Router Cache,这是一个按 individual route 端分割的独立内存缓存。Router Cache 通过存储之前访问过的路由和 prefetching future routes 改善导航体验

5、后续导航Subsequent Navigations

在后续导航或 prefetching 期间,Next.js 将检查路由器缓存中是否存储了 React Server Component Payload。如果存在,它将跳过向服务器发送新请求。

如果路由端 route segment 不在缓存中,Next.js 将从服务器获取 RSC Payload,并在客户端填充路由器缓存

静态和动态渲染

路由是否在构建时缓存取决于它是静态渲染还是动态渲染。静态路由默认被缓存,而动态路由在请求时渲染,不被缓存

持续时间 Duration

Full Router Cache 是持久的

失效

有两种方式可以使 Full Router 缓存失效:

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

退出 Opting out

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

  • 使用动态 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 配置选项可以用来调整上述自动失效时间。

失效

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

  • 服务器操作中:

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

退出选项

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

值得注意的是:你也可以通过将 <Link> 组件的 prefetch 属性设置为 false 来退出预取

缓存交互

在配置不同的缓存机制时,理解它们之间如何交互很重要:

数据缓存和完整路由缓存

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

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

  • 要立即使数据缓存和路由器缓存失效,你可以在服务器操作中使用 revalidatePath 或 revalidateTag
  • 路由处理程序中重新验证数据缓存不会立即使路由器缓存失效,因为路由处理程序不与特定路由关联。这意味着路由器缓存将继续提供之前的负载,直到强制刷新或自动失效期已过。