首页 / 文章 / 了解 Next.js 16 的 “use cache” 指令与基于标签的重新验证机制

了解 Next.js 16 的 “use cache” 指令与基于标签的重新验证机制

了解 Next.js 16 中“use cache”指令的运作原理、其配套的重新验证功能,以及如何在多租户应用中实现针对不同租户的缓存策略。

1215 词

在 Next.js 中,缓存功能传统上一直像是个黑箱——由文件级设置、传递给 fetch 的参数以及框架默认值共同构成,而这些默认值在不同版本间变化较大,以至于就连经验丰富的开发者也会为了保险起见随时查阅文档。Next.js 16 引入的 "use cache" 指令属于更广泛的缓存组件模型,它允许你在组件或函数层面明确声明缓存行为,而无需再依赖那些需要记忆的默认设置。

下面将详细说明该指令的实际功能以及适合使用它的场景。

该指令的实际功能

在函数或组件中使用 "use cache" 可让 Next.js 对该函数返回的内容进行缓存。从概念上讲,它的作用与 "use client" 类似,不同之处在于它并非标记客户端边界,而是标记缓存边界:

async function getDashboardStats(tenantId: string) {
  "use cache";
  const stats = await db.query.stats.findMany({ where: { tenantId } });
  return stats;
}

函数的输出会根据传入的参数被缓存并生成键值,之后会在后续请求中重复使用,直到有新数据使其失效。这与以往仅对单个获取请求或整个路由段进行缓存的方式有了显著不同:现在你可以根据数据的特性选择合适的缓存粒度,甚至可以针对某个特定函数进行缓存。

三个辅助函数

除了该指令之外,Cache Components 还提供了一组简单的 API,用于主动管理缓存数据,而无需等待定时器到期:

  • revalidateTag(tag) — 清除与指定标签相关的所有缓存条目。当某次数据变更影响了多个不同缓存函数所依赖的数据时,此功能非常有用。
  • updateTag(tag) — 是同一概念的简化版本,旨在更新与某个特定标签相关的缓存条目,而非该标签下的所有内容。
  • refresh() — 刷新当前请求范围内的缓存数据。

让整个系统顺畅运行的关键在于:为每个缓存的函数添加有意义的标签——比如 tenant-statsinvoice-list——每当发生影响这些底层数据的变更时,就使用对应的标签调用 revalidateTag,而非试图设定过短(违背缓存意义)或过长(导致使用过期数据)的基于时间的失效时间。

async function getInvoices(tenantId: string) {
  "use cache";
  cacheTag(`invoices-${tenantId}`);
  return db.query.invoices.findMany({ where: { tenantId } });
}
// After creating an invoice:
async function createInvoice(data: InvoiceInput) {
  await db.insert(invoices).values(data);
  revalidateTag(`invoices-${data.tenantId}`);
}

这种在读取时加标签、在写入时重新验证的组合,实际上就是整个机制的精髓所在。使用该系统时的其他几乎所有操作都是对这种配对的变体而已。

常见混淆点

团队最常犯的错误是认为“使用缓存”可以直接替代fetch()中的next: { revalidate }选项。实际上,它们解决的是不同但相关的问题。fetch()层面的选项控制的是单个网络请求,而“使用缓存”则会对整个函数或组件的结果进行缓存,这些函数或组件内部可能执行许多操作——访问数据库、进行计算,甚至可能在过程中再次调用fetch。如果只需要缓存一次外部API请求,使用fetch()层面的缓存通常更为简单。只有当需要缓存整个计算结果而非仅其输入的单一请求时,“使用缓存”才会体现出优势。

第二个常见的错误是完全跳过标记步骤,之后当某个变更未能清除本应删除的缓存数据时就会感到困惑。如果没有附加标记,唯一的失效机制就是时间,而这恰恰削弱了采用该模型的初衷——你增加了显式缓存的复杂性,却无法实现对数据失效的明确控制。

多租户应用中的租户感知标记

如果正在开发多租户系统,有一个重要细节需要特别注意:标签不仅要标识缓存数据的类型,还需体现对应的租户信息。如果使用如 invoices 这样的通用标签供所有租户共享,那么一旦某个租户的数据失效,所有租户的相关数据都会同步失效——这既可能引发正确性问题(由于其他租户的操作触发了重新验证,导致某个租户看到过时数据),也可能造成性能问题(缓存被清除的频率远高于必要程度)。正如上文示例中所示的 invoices-${tenantId} 这种格式并非出于风格考虑,而是区分有效失效策略与无效策略的关键。

为何从一开始就正确设置这一点至关重要

存在细微缺陷的缓存层很少会以明显的方式出问题。相反,它往往表现为诸如“为何这个控制面板仍显示上周的数值”之类的模糊支持工单——这类错误极难追踪,因为出问题的失效路径往往位于数月来无人触碰过的地方。尽早制定统一的标记策略,并在代码库中的每个缓存函数中一致应用,正是那种一开始妥善处理成本较低、但日后修复代价极高的基础设施决策之一。

一些仪表板入门套件正是基于这一规范来构建其数据层的。例如,Ovyqen为Next.js和SaaS项目设计的仪表板模板就始终采用“读取时添加标签、写入时重新验证”的模式,从一开始就将租户标识符嵌入到每个标签中,而非在跨租户缓存出错后才进行修补。如果您正在考虑不同的仪表板模板,而现有应用中缓存失效问题是大家最不信任的部分,那么从已经正确处理该问题的基础架构开始构建就是一个合理的选择。

常见问题

是否应该将所有的 fetch() 调用都迁移到 "use cache" 并非必须——这两种方式存在重叠,但并不能互相替代。对于简单的单次请求场景,使用 fetch 级别的缓存依然足够。只有当需要缓存计算结果或整个组件的输出时,才应使用 "use cache"

"use cache" 是否已经成熟到可以用于生产环境中的仪表板? 应该像对待任何较新的缓存机制那样对待它——在将其用于任何对数据时效性有要求的客户界面功能之前,务必充分测试其失效机制,尤其是针对多租户数据的情况。

如果缓存的函数未被添加标签会怎样?缓存仍然会发生,但你将无法针对特定事件主动使该缓存失效。此时你只能依赖基于时间的过期机制,而这往往并非你真正期望的行为。

显式缓存需要比单纯依赖框架默认设置更多的前期工作。但对于那些显示过时信息会带来实际代价的仪表板数据而言,多付出这些努力几乎总是一种值得的选择。

相关阅读

  • 适用于生产级 App Router 应用的 20 种高级 Next.js 模式 — 学习二十种高级别的 Next.js 开发模式,涵盖服务器优先设计、流式处理、缓存、路由及性能优化等内容,助力构建更快、更具扩展性的生产级应用。
  • 前端工程如何从样式处理发展到系统级扩展 — 探讨了前端开发从基础的 HTML/CSS/JS 发展到组件架构、缓存技术、单仓库管理以及可观测性机制的演变过程,这些都是为可靠地服务数百万用户所必需的。