Published on

Next.js App Router 中 ISR 的真实行为与设计取舍:为什么你的页面没有更新

Reading time
11 分钟
Page view
-
作者

问题从哪里开始

接口数据更新了,为什么首页内容没有更新?

近期在维护某内容型网站时遇到了一个问题,在测试过程中,我发现首页在数据库新增数据后没有发生变化。 起初我以为是 Cloudflare 对 API 的缓存导致的,使用 Postman 请求接口后发现数据已经完成了更新。 于是我又去排查了前后端部署平台的缓存,但均未发现问题,在重新编译前端代码并部署后发现首页数据更新到了最新状态,最后我将故障范围缩小到了 Next.js 缓存机制上。

一个常见但危险的等式

接口是动态 ≠ 页面会更新

通常情况下,我们会下意识认为接口数据的变化会直接导致页面内容变化,即 接口数据更新 -> 页面请求得到新数据 -> 页面内容更新。 但在某些场景中,这个渲染流程并不成立,有一个很容易被忽视的地方,那就是页面是否被重新生成,也就是说数据变化和页面生成之间是存在断层的。

我们很容易将 Next.js 的 App Router 数据获取当成自动响应式系统,在官方文档中有这样一段描述:

Caching is a technique for storing the result of data fetching and other computations so that future requests for the same data can be served faster, without doing the work again. While revalidation allows you to update cache entries without having to rebuild your entire application.

从中可以看出,Next.js 的页面生成结果可以被复用。

ISR 的出现是为了解决什么问题

在 ISR 之前,内容型站点面临的两难:

  • 纯 SSG:页面稳定但无法及时更新,用户看到的内容总是滞后的
  • 纯 SSR:用户看到的内容及时,但会带来较高的服务器负载和响应时间

不同于电商等对实时性要求较高的网站,内容型站点通常对数据的实时性要求并不高,但也不能永远不变,ISR 的出现正是为了解决这个问题。 ISR 的设计初衷是为了在 SSG 和 SSR 之间找到一个平衡点,让页面既能享受静态生成的性能优势,又能在一定时间内保持内容的更新。

ISR 的设计假设:

  • 延迟是可接受的
  • 不一致是短暂的

ISR 的核心目标是用时间换稳定,用可预期的延迟换部署与缓存的优势,它并不是为频繁变动数据设计的机制,ISR 天然不追求实时。

ISR 的真实工作方式

到底什么时候更新页面? 不是“每 N 秒自动更新”,而是超过 N 秒后,“下一个访问”触发更新

不同于第一印象,ISR 并不是一个后台定时刷新机制,当 revalidate 周期到达时,已生成的页面并不会立刻更新,而是等待下一次用户访问该页面时触发更新。

举一个例子:

  • 某页面设置了 revalidate: 60,表示页面内容每 60 秒可以更新一次
  • 用户 A 在 12:00:00 访问页面,页面被生成并缓存
  • 用户 B 在 12:00:30 访问页面,页面内容仍然是用户 A 看到的内容
  • 用户 C 在 12:01:05 访问页面,revalidate 周期到达,触发页面重新生成,但用户 C 仍然看到的是旧内容
  • 用户 D 在 12:01:10 访问页面,页面内容更新为最新内容,此更新是由用户 C 的访问触发

从上面的例子可以看出,ISR 的页面更新是被动触发的,只有在用户访问时才会检查是否需要重新生成页面。也就是说 延迟可见 是刻意设计而不是缺陷。

ISR 相比其他渲染策略的优势与代价

这不是进化路线,而是权衡取舍

ISR 相比 SSG:

  • 解决了内容长期不变的问题,页面能够在一定时间内更新,但不是实时的
  • 保留了静态生成的缓存优势,响应速度快,服务器负载低

ISR 相比 SSR:

  • 减少了服务器负载,内容稳定且可缓存,降低了成本
  • 放弃了内容的实时性,存在延迟和不一致性,不能够保证每次访问最新

ISR 的行为可预期且成本可控,但不适合强一致、强实时的场景。ISR 并不是 SSR/SSG 的升级版本,而是一种选择

dynamic vs revalidate

dynamic 不是“bug 开关”,而是不同场景的解决方案

在 Next.js App Router 中,dynamicrevalidate 是两种不同的渲染策略,dynamic 决定“是否每次渲染”,revalidate 决定“是否允许复用旧页面”。

当一个站点的内容每次访问都必须反应最新状态,或内容和请求上下文强相关时,延迟可见 是不可接受的,dynamic 在每次访问时重新渲染,很好地适配了这个场景。 而当页面内容相对稳定时,revalidate 提供了一个合适的方案,在允许一定延迟的前提下,用时间换取稳定性。

当我发现站点的首页内容没有及时更新时,也许可以选择 dynamic 来确保每次访问都能获取最新内容。这看起来好像解决了问题,但实际上转移了问题的场景。 dynamicrevalidate 并不是互斥的选择,而是根据不同需求选择合适的渲染策略。

页面级 revalidate vs 接口级 revalidate

粒度粗细取决于一致性需求,而不是谁更灵活

设想一个新闻站点的首页,包含多个模块:今日新闻、昨日新闻等等。当这两个模块从独立接口获取数据,并采用接口级的 revalidate 时,可能会出现这样的情况: 今日新闻没更新,但昨日新闻更新了,如果已经跨天,甚至可能出现昨日新闻比今日新闻更新的割裂场景,这会直接影响站点的可信度。

对于多接口聚合的页面来说,接口级的 revalidate 很难保证页面内容的一致性,聚合页面作为最终交付物,应当承担起一致性的责任,此时页面级 revalidate 更有优势。 同理,如果某页面的内容来自多个接口,但不同模块间并没有强抑制性要求,那么也许可以考虑接口级 revalidate 来获取更高的性能。 总的来说,选择哪种粒度的 revalidate,应当基于页面内容的一致性需求,而不是谁更灵活。

低访问量内容站点中的 ISR 行为

网站为什么看起来像是坏掉了?

前文提到 ISR 可以设置 revalidate 让用户在下一次的访问中触发页面更新,当站点访问量很低时,页面的更新频率会大幅降低,甚至可能出现页面长期不更新的情况。 用户访问页面时,看到的内容可能是几天甚至十几天前的旧内容,这会让用户觉得网站“坏掉了”。

实际上,这正是 ISR 设计的结果,用户 A 访问页面时见到的内容可能是很久以前生成的,用户 A 的此次访问触发了页面的重新生成。 但是由于低访问量站点的用户访问间隔可能长达几天,在下一个用户 B 访问时,页面内容是上次用户 A 触发更新后的内容,此时用户 B 看到的内容仍然是旧的。

对于低访问量的内容站点来说,访问量可能会决定内容的更新频率,只要这是最初的设计目标且可接受,那么这种 惰性更新 行为就是合理的,也并不代表网站“坏掉了”。

ISR 是否会导致页面不同步

这不是一个需要被过度恐惧的问题

诚然,在实际应用 ISR 时可能会遇到页面内容不同步的场景,但这是一个需要被理解和接受的设计取舍,而不是一个需要被过度恐惧的问题。 ISR 的设计天然允许短暂的不一致,相比于“是否会出现不一致”,更重要的是这种不一致是不是可预期的。 在设计过程中,应当明确内容站点对一致性的真实需求,分析不一致出现的阶段和不一致可能带来的影响是否可接受,而不是盲目追求及时性或一致性。 当内容的不一致是可控且可预期的,“页面不同步”就不是一个需要过度担忧的问题,而是设计中的一个已知因素。

以时间驱动内容的 ISR 策略

以我维护的站点首页为例,首页聚合了“未来内容”和“最新内容”模块,这是以时间为驱动的内容:“未来内容”管理今天之后的内容,“最新内容”管理今天和之前的内容。

这就会遇到一致性的取舍问题,如果选择接口级 revalidate,会是某模块的缓存策略独立于页面存在,首页就变成了一种“拼装快照”,对于我的首页来说,这是一种不自然的状态,也就是不可接受的。 此外,首页结构稳定、变化仅发生在内容层,因此更适合页面级 ISR 作为整体失效策略。页面级策略可以让我的页面作为一个整体,同步更新,同步失效,不存在“一部分新、一部分旧”,这和我的业务语义是一致的。

我的站点数据更新在一天内的某个时间点集中发生,一天内只有一个关键变化窗口,其他时间数据基本是稳定的。如果 revalidate 设置为一天,很可能导致用户在关键窗口前访问,而之后整整一天都不再刷新。 由于站点访问量很低,revalidate 对我来说实际上像是“我每天允许这个页面最多错过多少次访问机会”,考虑到首页并不需要分钟级实时,也不希望半天或一整天都是旧数据, 我将 revalidate 设置为 1 小时,这样即使访问量很低,一天也有 24 次机会触发页面更新,只要有用户在关键窗口后访问一次,基本就能够命中更新后的数据。

为什么不使用更小的值,比如 5 分钟?因为根据项目实际情况判断并没有必要,revalidate 设置为 5 分钟的真实效果就是,页面实际效果接近动态渲染,却保留了 ISR 的复杂度而不带来明显受益,最重要的是业务能够接受当前临界点的延迟。 对我来说,ISR 的 revalidate 并不是一个缓存参数,而是对业务节奏的一种建模。

何时从 ISR 过渡到 on-demand revalidate

是否过渡到 on-demand revalidate,不取决于技术成熟度,而取决于“内容更新 -> 用户可见”这条链路是否开始影响用户信任

我现在的站点更新频率只有每天一个关键窗口,访问量很低,且用户心智可能处于探索或偶发访问,对内容的期望更倾向于“稳定”而非“及时”,如果站点出现了以下情况:

  • 用户开始把我的站点当成“准实时信息源”
  • 数据更新频率提升/不规则
  • 用户频繁反馈内容过时

此时就说明我的站点内容从时间驱动变成了事件驱动,用户对内容的实时性要求提升了,这时就需要考虑从 ISR 过渡到 on-demand revalidate 机制。 换句话说,当“更新时间的不确定性”开始被用户感知时,就该考虑更改渲染策略了,on-demand revalidate 是站点新阶段的不同策略,而不是 ISR 的补丁。

结语

ISR 不是不可靠,只是被放在了错误的预期里

当站点的内容可以容忍延迟和短暂的不一致,访问频率不稳定时,ISR 才成立。软件工程没有银弹,ISR 也不是万能的。关键不在于是否使用 ISR,而在于是否接受它的延迟与不一致,理解 ISR 的设计取舍,并根据业务需求选择合适的渲染策略。


参考资料