我们正在为 Next.js 开发一套简单而强大的缓存模型。在之前的文章中,我们探讨了缓存探索之旅以及如何最终实现 'use cache'
指令。
本文将重点讨论 'use cache'
的 API 设计及其优势。
什么是 'use cache'
?
'use cache'
通过按需缓存数据或组件来提升应用性能。
这是一个 JavaScript "指令"——您添加到代码中的字符串字面量——它会告知 Next.js 编译器进入不同的"边界"。例如,从服务端切换到客户端。
这与 React 的 'use client'
和 'use server'
等指令理念相似。指令是定义代码运行位置的编译器指示,让框架能为您优化和编排各个代码片段。
工作原理
从一个简单示例开始:
在底层,由于 'use cache'
指令,Next.js 会将此代码转换为服务端函数。编译期间,会找到该缓存条目的"依赖项"并将其作为缓存键的一部分。
例如,id
成为缓存键的一部分。如果我们多次调用 getUser(1)
,会返回缓存服务端函数的记忆化输出。更改此值将在缓存中创建新条目。
再看一个在服务端组件中使用缓存函数和闭包的例子:
这个例子更复杂。您能找出需要作为缓存键的所有依赖项吗?
参数 index
和 limit
很直观——如果这些值改变,我们会选择通知的不同分片。但用户 id
呢?它的值来自父组件。
编译器能理解 getNotifications
也依赖于 id
,其值会自动包含在缓存键中。这避免了因缓存键中依赖项错误或缺失导致的整个缓存问题类别。
为何不使用缓存函数?
回顾上一个例子。我们是否可以用 cache()
函数替代指令?
cache()
函数无法查看闭包并识别 id
值应作为缓存键的一部分。您需要手动指定 id
为键的一部分。如果忘记或操作不当,可能导致缓存冲突或数据过时。
闭包可以捕获各种局部变量。简单处理可能会意外包含(或遗漏)您未预期的变量。这会导致缓存错误数据,或者如果敏感信息泄露到缓存键中,可能引发缓存污染风险。
'use cache'
为编译器提供了足够上下文来安全处理闭包并正确生成缓存键。纯运行时方案(如 cache()
)需要您手动完成所有操作——很容易出错。相比之下,指令可通过静态分析可靠处理所有底层依赖。
如何处理不可序列化的输入值?
我们有两种不同类型的缓存输入值:
- 可序列化:指输入可转换为稳定、基于字符串的格式且不丢失意义。虽然很多人首先想到
JSON.stringify
,但我们实际使用 React 的序列化(如通过服务端组件)处理更广泛的输入——包括 Promise、循环数据结构和其它复杂对象。这超出了普通 JSON 的能力范围。 - 不可序列化:这些输入不是缓存键的一部分。尝试缓存这些值时,我们会返回服务端"引用"。Next.js 在运行时使用该引用恢复原始值。
假设我们记得在缓存键中包含 id
:
如果输入值可序列化,这能正常工作。但如果 id
是 React 元素或更复杂的值,我们就需要手动序列化输入键。考虑一个基于 id
属性获取当前用户的服务端组件:
逐步解析其工作原理:
- 编译期间,Next.js 看到
'use cache'
指令并将代码转换为支持缓存的特殊服务端函数。编译时不进行缓存,而是 Next.js 建立运行时缓存所需的机制。 - 当代码调用"缓存函数"时,Next.js 序列化函数参数。任何不能直接序列化的内容(如 JSX)会被替换为"引用"占位符。
- Next.js 检查给定序列化参数是否存在缓存结果。如果未找到结果,函数会计算新值进行缓存。
- 函数完成后,返回值被序列化。返回值的不可序列化部分会转换回引用。
- 调用缓存函数的代码反序列化输出并评估引用。这使得 Next.js 能用实际对象或值替换引用,意味着像
children
这样的不可序列化输入可以保持其原始、未缓存的值。
这意味着我们可以安全地仅缓存 <Profile>
组件而不缓存子组件。在后续渲染中,不会再次调用 getUser()
。children
的值可能是动态的,或是具有不同缓存生命周期的单独缓存元素。这就是可组合式缓存。
似曾相识...
如果您觉得"这感觉与服务端和客户端组合的模式相同"——完全正确。这有时被称为"甜甜圈"模式:
- 外层甜甜圈是处理数据获取或重型逻辑的服务端组件
- 中间孔洞是可能具有交互性的子组件
'use cache'
同理。甜甜圈是外层组件的缓存值,孔洞是在运行时填充的引用。这就是为什么更改 children
不会使整个缓存输出失效。子组件只是稍后填充的引用。
标签与失效机制
您可以通过不同配置文件定义缓存生命周期。我们提供一组默认配置,但您也可以根据需要定义自定义值。
要使特定缓存条目失效,您可以标记缓存然后调用 revalidateTag()
。一个强大模式是您可以在获取数据后(例如从 CMS)标记缓存:
简单而强大
我们设计 'use cache'
的目标是让缓存逻辑编写既简单又强大。
- 简单:您可以通过局部推理创建缓存条目。无需担心全局副作用,如遗忘缓存键条目或意外更改代码库其它部分。
- 强大:您可以缓存不仅仅是静态可分析代码。例如,运行时可能变化的值,但您仍希望缓存评估后的输出结果。
'use cache
在 Next.js 中仍处于实验阶段。我们期待您试用后的早期反馈。