react中rust编译器开发目标、设计原则,架构备忘
本文档描述了 React 编译器的目标、设计原则和高层次架构。有关数据结构和编译器传递的具体细节,请参阅代码。
目标
React 编译器的理念是让开发者在保持 React 熟悉的声明式、组件化编程模型的同时,确保应用默认情况下具有快速性能。具体来说,我们寻求实现以下目标:
- 限制更新时的重新渲染量,以确保应用默认情况下具有可预测的快速性能。
- 保持启动时间与 React 编译器之前的性能持平。特别地,这意味着保持代码大小的增加和记忆化开销足够低,以不影响启动时间。
- 保留 React 熟悉的声明式、组件化编程模型。也就是说,解决方案不应从根本上改变开发者编写 React 的方式,而应通常移除概念(如使用 React.memo()、useMemo() 和 useCallback() 的需求),而不是引入新概念。
- 在遵循 React 规则(纯渲染函数、Hooks 规则等)的惯用 React 代码上“即插即用”。
- 支持典型的调试和分析工具及工作流程。
- 对 React 开发者来说是可预测且易于理解的——即开发者应能够快速形成对 React 编译器工作原理的粗略直觉。
- 不需要显式注解(类型或其他)用于典型产品代码。我们可能会提供允许开发者选择使用类型信息以启用额外优化的功能,但编译器应在没有类型信息或其他注解的情况下也能良好工作。
非目标
以下是 React 编译器明确不追求的目标:
- 提供完美优化的重新渲染,完全避免不必要的重新计算。出于以下几个原因,这是一个非目标:
- 额外跟踪的运行时开销在许多情况下可能超过重新计算的成本。
- 在具有条件依赖的情况下,可能无法避免重新计算某些/所有指令。
- 代码量可能会增加启动时间,这与我们保持启动性能中性的目标相冲突。
- 支持违反 React 规则的代码。React 的规则旨在帮助开发者构建健壮、可扩展的应用程序,并形成一个允许我们不断改进 React 而不破坏应用程序的契约。React 编译器依赖这些规则来安全地转换代码,因此违反规则将破坏 React 编译器的优化。
- 支持遗留的 React 特性。特别地,我们不会支持类组件,因为它们的固有可变状态在多个具有复杂生命周期和数据流的方法之间共享。
- 支持 100% 的 JavaScript 语言。特别是,我们将不支持很少使用的特性,以及已知不安全或无法被合理建模的特性。例如,捕获其闭包值的嵌套类由于可变性而难以准确建模,
eval()是不安全的。我们的目标是支持绝大多数 JavaScript 代码(以及 TypeScript 和 Flow 方言)。
设计原则
设计的许多方面自然地遵循上述目标:
- 编译器输出必须是保留输入语义的高级代码,并且使用与输入类似的构造来表达。例如,我们保留逻辑表达式(
a ?? b)的高级形式,而不是将其转换为if语句。我们保留循环构造的原始形式,而不是将其转换为单一形式。这遵循我们的目标:- 高级代码更紧凑,有助于减少编译对应用程序大小的影响
- 与开发者编写的内容匹配的高级构造更易于调试
- 由此,编译器的内部表示也必须足够高级,以便能够输出原始的高级构造。我们称之为高级中间表示(HIR)——这个名字借用了 Rust 编译器。然而,React 编译器的 HIR 可能更适合这个名字,因为它保留了高级信息(区分 if、逻辑、三元运算符,或 for、while、for..of),但也以无嵌套的控制流图表示代码。
架构
React 编译器有两个主要的公共接口:用于转换代码的 Babel 插件和用于报告 React 规则违反情况的 ESLint 插件。在内部,两者使用相同的编译器核心逻辑。
编译器的核心在很大程度上与 Babel 解耦,使用自己的中间表示。高层次流程如下:
- Babel 插件:根据插件选项和任何本地选择加入/退出指令,确定文件中的哪些函数应被编译。对于每个要编译的组件或 Hook,插件调用编译器,传入原始函数并获取一个新的 AST 节点,该节点将替换原始节点。
- Lowering(构建 HIR):编译器的第一步是将 Babel AST 转换为 React 编译器的主要中间表示 HIR(高级中间表示)。这一阶段主要基于 AST 本身,但目前依赖 Babel 来解析标识符。HIR 保留了 JavaScript 的精确求值顺序语义,解析 break/continue 到它们的跳转点等。生成的 HIR 形成一个基本块的控制流图,每个基本块包含零个或多个连续指令,后跟一个终端。基本块以逆后序存储,使得前向迭代基本块允许在访问后继者之前访问前驱者,除非存在“回边”(即循环)。
- SSA 转换(进入 SSA):将 HIR 转换为 SSA 形式,使得 HIR 中的所有标识符都更新为基于 SSA 的标识符。
- 验证:我们运行各种验证传递,检查输入是否是有效的 React,即它不违反规则。这包括查找条件 Hook 调用、无条件 setState 调用等。
- 优化:各种传递如死代码消除和常量传播通常可以提高性能并减少需要进一步优化的指令数量。
- 类型推断(推断类型):我们运行一个保守的类型推断传递,以识别程序中可能出现的某些关键类型的数据,这些数据对进一步分析是相关的,例如哪些值是 Hooks、原始类型等。
- 推断响应式作用域:涉及几个传递来确定一起创建/修改的值组以及创建/修改这些值的指令集。我们称这些组为“响应式作用域”,每个作用域可以有一个或多个声明(或偶尔的重赋值)。
- 构建/优化响应式作用域:一旦编译器确定了响应式作用域的集合,它就会转换程序以在 HIR 中显式表示这些作用域。代码随后被转换为 ReactiveFunction,它是 HIR 和 AST 的混合体。作用域进一步修剪和转换。例如,编译器不能使 Hook 调用成为条件性的,因此任何包含 Hook 调用的响应式作用域都必须被修剪。如果两个连续的作用域总是会一起失效,我们尝试合并它们以减少开销等。
- 代码生成:最后,ReactiveFunction 混合 HIR/AST 被转换回原始的 Babel AST 节点,并返回给 Babel 插件。
- Babel 插件:Babel 插件用新版本替换原始节点。
ESLint 插件的工作方式类似。目前,它实际上在代码上调用 Babel 插件,并返回一部分错误报告。编译器可以报告各种错误,包括代码只是无效的 JavaScript,但 ESLint 插件过滤仅显示 React 特定的错误。