Next.js缓存能力

March 25, 2024

前段时间在使用next.js搭建个人博客的时候遇到了一些性能上的问题,最早的实现使用prisma+postgres自己实现了数据缓存。后来发现在部署到生产环境以后效果并不理想,因为云端缓存跟本地的环境还是有差距,无法保障next后台和pg的时延,此外就是缓存过期时从notion取数据会有明显的卡顿,也没有找到xian后来看了了next的文档,发现其已经提供了不少缓存解决方案,降低对后台数据源的实时依赖。虽然有了一些内存上的上涨以及透明度上的隐患,但是相对自己去实现可能会更加合适。

本文重点介绍next.js的几种缓存机制。

简介

next.js的缓存机制实现比较复杂,可以分为多层,重点包括:

  • Router-Cache: 客户端(浏览器)上的缓存,主要目的是通过预拉取、缓存RSCP等减少客户端等待耗时。
  • Full Route Cache:服务端的渲染结果缓存,目的是减少静态路由的重复渲染。对于页面无变化的场景适用
  • React-Cache / Request-Memorization:对同一次(页面渲染)请求时多次拉取同一数据源的缓存,既能减少同一次渲染对某个数据源的多次拉取,也能避免代码上组件之间通过props传递共享数据的繁琐。
  • Data-Cache:类似后台的共享内存或者缓存中间件(redis等),主要用于缓存用户无关的数据。比如网站文章数量、内容以及访问数等,供多次请求间的数据共享。
  • 数据源:db或者cms等业务逻辑后台。这一层已经不属于缓存了。

Request memorization

Loading...

目的

  1. 同一次server request多个组件、页面等统一数据源的复用,避免一次渲染对数据源的重复请求,也实现了数据快照的目的。
  2. 避免多个component之间的共享props数据传递。

next.js默认会将fetch的结果缓存(准确的叫法为memorize)到内存(In-memory),供同一请求内的数据共享。注意这一操作对开发者是透明的,开发也不用担心一次页面渲染依赖的多个组件对同一数据源依赖时请求翻倍。

流程&原理

注意点

  • Request memorization 是React的一个特性,而不是Next.js的特性。虽然Next.js可能会包含这个特性,但它是React核心库的一部分。
  • 只适用于fetch请求中的GET方法,不适用post。因为GET通常用于数据请求,而POST用于数据写请求,不能被cache。
  • 只适用于React组件树。这意味着,只有在React组件树中的某些部分发起的fetch请求才会被memorization缓存。
    • generateMetadatagenerateStaticParamsLayoutsPages以及其他服务端组件中发起的fetch请求都会被memorization缓存。
    • 路由处理器(Route Handlers)中的fetch请求不会被记忆化,因为它们不是React组件树的一部分。
  • 对于不适合使用fetch的情况(例如,某些数据库客户端、内容管理系统(CMS)客户端或GraphQL客户端),你可以使用React的cache函数来代替。

有效时长以及更新(revalidation):

  • 仅在单次请求期间有效,组件数渲染完毕对应的memorization缓存就会被free掉。相当于对一个server request提供一份数据的快照。
  • 由于仅单次请求有效,通常不涉及数据更新机制。

如何取消(禁用机制):

  • 如果开发者想要管理单个请求(例如,取消某个正在进行的请求),可以使用AbortControllersignal属性。AbortController提供了一种取消fetch请求的方法,通过传递signal属性给fetch函数,可以在需要时取消请求。
  • 需要注意的是,使用AbortController并不会让请求不参与记忆化。它仅仅是用来取消正在进行的请求。即使你取消了某个请求,如果该请求是GET请求,它仍然会被记忆化。记忆化和取消请求是两个独立的功能

总结:

  1. Request-memorization的机制跟react-cache类似,仅用于处理同一个请求时共享数据的场景。
  2. 在使用fetch()函数GET方式时默认生效。

react-cache

准确的说react-cache属于React的特性,只不过在next中也可以使用,主要用于next的 fetch() 未覆盖的场景。下面是一些注意的点。

  1. reac-cache仅在服务端组件可以使用。且仅在服务器组件内(直接或者间接都可以)调用时才会生效。在组件外部调用时不会生效。比如:
Loading...
  1. cachedFn还会缓存错误。如果fn函数对于某些参数抛出了错误,这个错误会被缓存起来。当使用相同的参数再次调用cachedFn时,相同的错误会被重新抛出。这意味着错误不会被忽视,而是会被记住并在相同的情况下再次触发。
  2. 跟request的memorization一样,cache数据仅在同一server quest期间共享。这意味着,当服务器处理一个新的请求(即使来自同一个终端用户)时,它不会使用之前的缓存结果,而是会重新计算这些函数的结果。
  3. cache可以用作服务端的数据预拉取。比如进入页面时先调用async的(不用await结果)cacheFn,服务端就会进入预拉取阶段。后面页面再次调用时就可以直接拿到缓存结果,从而起到加速的效果。
Loading...
  1. 注意:cache对函数的参数也有限制。如果参数类型是object等复杂类型,需要确保传入的是同一个引用,因为react-cache内部仅做shallow equality对比,猜测是出于性能以及安全性的考虑。
🌐 If your arguments are not [primitives] (ex. objects, functions, arrays), ensure you’re passing the same object reference.When calling a memoized function, React will look up the input arguments to see if a result is already cached. React will use shallow equality of the arguments to determine if there is a cache hit.

总结:

react cache与上面Next的request memorization功能基本一样,是对next下fetch能力限制的补充。也仅适合在处理同一个请求的的重复数据源拉取场景,比如generateMetadata和Page渲染使用了同一份统数据(比如需要查询db共享数据)。而业务逻辑上极少有需要取两次同一份数据的场景,有也可以通过代码(比如props透传)规避。当然相对通过组件间props透传共享数据还是更加方便了许多。

data-cache

为了做到多个server request之间共享数据(cache),Next.js 提供了一个内置的数据缓存(data cache),可以在传入的服务器请求部署中持久化数据。之所以能做到这一点,是因为 Next.js 扩展了本地 fetch API,以允许服务器上的每个请求设置自己的持久缓存语义。

默认情况下,使用 fetch 请求的数据请求结果出了被memorization外,也会被data-cache缓存。可以通过fetch参数中的cachenext.revalidate控制其行为。

Loading...

流程及原理:

注意点:

  • 在渲染过程中首次调用 fetch 请求时,Next.js 会检查data cache是否有缓存响应(前提是request memorization缓存miss了才会走到data-cache
  • 如果data-cache命中缓存,会立即返回并写入到request memorization
  • 如果data-cache未命中,则会向后端数据源发出请求,并将返回结果存储在data cache request memorization中。data cache的数据是为了供后续其他server request来复用。
  • 对于指定了不缓存的数据(如fetch设置{ cache: 'no-store' }参数),会跳过data-cache的读写,总是从数据源获取数据,并加入request memorization。
  • 无论data cache是否缓存,request-memorization都会为本次请求memorize数据,避免同一客户端请求多次访问同一数据源。

🌐 数据缓存与请求记忆化的区别:

  • 场景:虽然这两种缓存机制都能通过重复使用缓存数据来提高性能,但数据缓存(data cache)是跨传入请求和部署的持久性缓存,而记忆化(memoriztion)则只在单次请求的生命周期内有效。
  • 介质:memorization使用内存,data-cache默认使用文件系统。

有效时长

部署环境默认永久有效,除非指定opt-out、revalidation机制。

缓存失效机制(revalidation)

  • 基于时间(被动)

要按时间间隔重新验证数据,可以使用 fetch 的 next.revalidate 选项来设置资源的缓存时间(以秒为单位)。即超过指定的时长之后,会重新从数据源获取,并重新写入data cache和memorization。

Loading...

注意:当data cache数据过期后,为了不阻塞请求,本次会优先返回过期的数据。然后异步执行更新机制。这样下次就可以使用到更新后的数据。类似stale-while-revalidate性能考虑的优化,但是对请求量少且数据时效性有较高要求的场景可能会是个问题

  • 按需触发(主动)

数据可按路径(revalidatePath)或缓存标签(revalidateTag)执行revalidation。

Loading...

与基于时间的模式不同,执行revalidation之后数据会立即过期,当下一次访问时需要先从数据源获取数据,没有旧数据兜底。

按需触发存在两个场景

  1. root-handler:比如提供webhook给到数据源,当数据源变更时主动通知变更。
  2. Server action:用户操作触发

如何取消(禁用机制):

有两种方法可以禁用data-cache:

  • 对于单个请求,可以在fetch时指定 no-store 参数
  • 路由段配置选项,禁用某个route下的所有data-cache,使用一下方法之一都可以禁用data-cache
Loading...

🌐 Vercel Data Cache:If your Next.js application is deployed to Vercel, we recommend reading the Vercel Data Cache documentation for a better understanding of Vercel specific features.

总结:

  1. data-cache是request memorization的扩展,可以做到在不同请求间共享数据。
  2. data-cache通常对使用方是透明的,只要是通过fetch() 函数GET方式获取数据时默认生效。如果对数据有实时性的要求需要主动禁用或者设置revalidation更新。
  3. data-cache默认使用文件系统存储,另外也提供了对外扩展的能力(cache-handler)。在官方的example中也有给出使用redis、内存来实现cache-handler的例子。
In Next.js, the default cache handler for the Pages and App Router uses the filesystem cache. This requires no configuration, however, you can customize the cache handler by using the cacheHandler field in next.config.js.

unstable_cache

对于未使用 fetch或者使用了HTTP POST的场景(比如通过sdk访问第三方数据、查询db结果等),可以使用nextjs提供的 unstable_cache 来访问来存取data cache。使用方式与react-cache类似,另外也提供了tag、revalidate等更新机制。详见文档。 unstable_cache 还处于实验室阶段,后续可能存在变更的可能。

next-shared-cache上看到了类似的解决方案,可以将数据cache到Redis等外部存储,适应多机共享存储的场景。

Full Route Cache

通过在构建时预先渲染和缓存路由,Next.js 实现了所谓的“静态生成”(Static Generation)。这意味着,当用户请求一个已缓存的路由时,服务器可以直接从缓存中提供 HTML,而不需要重新渲染。这显著减少了处理请求所需的时间,从而提高了页面的加载速度

某些page在build阶段就会生成并被Full Route Cache缓存,前提是该页面不包含任何动态数据(无外部数据依赖或者仅依赖 fetch )。动态数据是指在运行时可能改变的数据,如用户输入、数据库查询结果等。页面的HTML和RSCP会被缓存,避免重新执行渲染操作。只有当重新部署应用程序或使该页面依赖的数据缓存失效时,这些缓存的内容才会被更新。

你可能认为,因为我们正在进行一个 fetch 请求,所以我们拥有动态数据。但实际上,这个 fetch 请求被 Next.js 在数据缓存(Data Cache)中缓存了,因此这个页面实际上是被视为静态的。动态数据是指每次请求页面时都会变化的数据,例如dynamic URL 、cookies、请求头、search param等。

流程&原理:

对于符合条件的路由(静态)路由,rendering的结果(RSCP、HTML)会被写入Full Route Cache。下次的访问就不会重新执行渲染了。

有效时长:

部署环境默认永久有效,除非指定revalidation机制。

更新机制(revalidation)

有两种机制可以revalidate Full Route Cache:

  1. Revalidate Data Cache;对Data cache的数据执行更新操作,会使得对应的rooter cache更新。
  2. 重新部署

🌐 Redeploying: Unlike the Data Cache, which persists across deployments, the Full Route Cache is cleared on new deployments.

如何取消(禁用机制):

可以通过以下方法要求每次访问都重新执行渲染:

  • 引入动态函数:即存在fetch+HTTP以外的数据依赖
  • 路由段配置选项:与data-cache方法一样,禁用某个route下的所有data-cache时,也会对full route cache生效。

Router-cache

Next.js 具有一个内存中的客户端缓存,称为“路由器缓存”(Router Cache)。这个缓存用于存储 React 服务器组件的负载(React Server Component Payload),这些负载会根据不同的路由段进行分割,并在用户的会话期间保持存储状态。

原理

  1. 当用户在应用中导航到不同的页面或路由时,Next.js会缓存这些已访问的路由段(route segment)。这意味着当用户决定返回到之前访问过的页面时,这些页面可以迅速地从缓存中加载,而不是重新生成。
  2. Next.js还会根据用户在视口(viewport)中的<Link>组件来预测用户可能会导航到的路由,并预先加载这些路由。这确保了当用户决定导航到这些页面时,这些页面已经准备好了,从而实现了快速导航。——预加载这些比较常见的优化手段,next都集成在框架内了。但是这里也有考虑对服务端的无效流量。

总之,在传统的Web应用中,当用户导航到新的页面时,整个页面通常都会重新加载。但在Next.js中,由于路由缓存和预取,这种全页重载被避免了,从而为用户提供了更流畅的体验。

路由器缓存(Router Cache)和全路由缓存(Full Route Cache)之间的主要区别:

  1. 存储位置
    • 路由器缓存(Router Cache):这种缓存暂时在用户的浏览器会话期间存储React服务器组件的负载。它是内存中的客户端缓存,这意味着数据存储在用户的浏览器内存中,而不是在服务器上。
    • 全路由缓存(Full Route Cache):这种缓存持久地在服务器上存储React服务器组件的负载和HTML,跨越多个用户请求。这意味着数据存储在服务器端的持久存储中,而不是在用户的浏览器内存中。
  2. 缓存内容
    • 路由器缓存:它存储的是React服务器组件的负载,这些负载是根据不同的路由段分割的。
    • 全路由缓存:它不仅存储React服务器组件的负载,还存储HTML内容。
  3. 缓存策略
    • 路由器缓存:它适用于静态渲染和动态渲染的路由。这意味着无论路由是如何渲染的(静态或动态),路由器缓存都会存储相关的负载。
    • 全路由缓存:它只适用于静态渲染的路由。这意味着它不会缓存动态渲染的路由的负载或HTML。
  4. 缓存生命周期
    • 路由器缓存:它的生命周期与用户的会话绑定。一旦用户的会话结束,缓存的数据就会被清除。
    • 全路由缓存:它的生命周期跨越多个用户请求,并且会持续存在,直到被显式地清除或由于某种原因(如服务器重启)而失效。

生效时长:

路由器缓存的持续时间由两个因素决定:

  • 会话:缓存在浏览过程中持续存在。不过,它会在页面刷新时被清除。
  • 自动失效期:单个路由段的缓存会在特定时间后自动失效。持续时间取决于路径是静态渲染还是动态渲染:
    • 动态渲染:30 秒
    • 静态渲染:5 分钟

缓存失效(revalidation)

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

  • 服务器操作中:
    • 使用(revalidatePath)按路径或(revalidateTag)按缓存标签按需失效数据
    • 使用 cookies.set 或 cookies.delete 可使路由器缓存失效,以防止使用 cookies 的路由(例如:身份验证)变得过时
  • 调用 router.refresh 会使路由器缓存失效,并就当前路由向服务器发出新请求

缓存退出

  1. 退出路由缓存

你不能选择完全退出路由缓存,因为Next.js设计这个缓存机制是为了提高性能和导航体验。但是,你可以通过调用一些方法来使缓存失效(invalidate the cache):

  • router.refresh(): 这个方法会清除当前路由的缓存,并向服务器发送一个新的请求来获取最新的数据。
  • revalidatePath(): 这个方法允许你指定一个路径,并清除该路径对应的缓存。
  • revalidateTag(): 类似于revalidatePath(),但它是基于标签(tag)来清除缓存的,允许你清除一组相关路由的缓存。
  1. 退出预取

你可以通过设置<Link>组件的prefetch属性为false来退出预取功能。这样做将阻止Next.js预加载该链接对应的页面。

然而,即使你禁用了预取,Next.js仍然会临时存储已访问的路由段,以便在30秒内实现嵌套段(如标签栏)之间的即时导航,以及前后导航。这意味着,即使你退出了预取功能,已访问的路由仍然会被缓存,以便快速回退或前进。

See all postsSee all posts