迈向 Hermes 成为默认引擎
自从 我们在 2019 年宣布 Hermes 以来,它在社区中的采用率越来越高。 Expo 团队维护着一个流行的 React Native 应用元框架,他们最近宣布了实验性 支持 Hermes,此前 Hermes 一直是 Expo 最受用户要求的功能之一。 Realm 团队,一个流行的移动数据库,最近也发布了对 Hermes 的 alpha 支持。在这篇文章中,我们想重点介绍我们在过去两年中为推动 Hermes 成为 React Native 最佳 JavaScript 引擎所取得的一些最令人兴奋的进展。展望未来,我们有信心,凭借这些改进以及未来更多的改进,我们可以使 Hermes 成为所有平台上 React Native 的默认 JavaScript 引擎。
为 React Native 优化
Hermes 最显著的特点是它如何提前执行编译工作,这意味着启用 Hermes 的 React Native 应用会附带预编译的优化字节码,而不是纯 JavaScript 源代码。这大大减少了用户启动产品所需的工作量。来自 Facebook 和社区应用的测量表明,启用 Hermes 通常可以将产品的 TTI(或 Time-To-Interactive)指标缩减近一半。
话虽如此,我们一直在努力改进 Hermes 的许多其他方面,使其成为更适合 React Native 的专用 JavaScript 引擎。
为 Fabric 构建新的垃圾回收器
随着即将到来的新 React Native 架构中的 Fabric 渲染器,可以在 UI 线程上同步调用 JavaScript。然而,这意味着如果 JavaScript 线程执行时间过长,可能会导致明显的 UI 帧丢失并阻塞用户输入。 React 18 启用的并发渲染 Fiber 将通过将渲染工作拆分为多个块来避免调度长时间的 JavaScript 任务。但是,JavaScript 线程延迟的另一个常见来源是当 JavaScript 引擎必须“停止世界”以执行垃圾回收 (GC) 时。
Hermes 之前的默认垃圾回收器 GenGC 是一个单线程的分代垃圾回收器。新生代使用典型的半空间复制策略,老生代使用标记压缩策略,使其非常擅长积极地将内存返回给操作系统。由于其单线程特性,GenGC 的缺点是会导致长时间的 GC 暂停。在像 Android 版 Facebook 这样复杂的应用上,我们观察到平均暂停时间为 200 毫秒,p99 时为 1.4 秒。考虑到 Android 版 Facebook 庞大且多样化的用户群,我们甚至见过长达 7 秒的暂停。
为了缓解这种情况,我们实现了一个全新的主要并发 GC,名为 Hades。 Hades 收集新生代的方式与 GenGC 完全相同,但它使用开始时快照风格的标记清除回收器来管理老生代。这可以通过在后台线程中执行大部分工作而不会阻塞引擎的主线程执行 JavaScript 代码来显着减少 GC 暂停时间。 我们的统计数据显示,Hades 在 64 位设备上的 p99.9 时仅暂停 48 毫秒(比 GenGC 快 34 倍!),在 32 位设备上的 p99.9 时暂停约 88 毫秒(在 32 位设备上,它作为单线程增量 GC 运行)。这些暂停时间的改进可能会以整体吞吐量为代价,因为需要更昂贵的写入屏障、更慢的基于空闲列表的分配(与 bump 指针分配器相反)以及增加的堆碎片。我们认为这些是正确的权衡,并且我们能够通过合并和我们将要讨论的额外内存优化来实现整体更低的内存消耗。
解决性能痛点
应用的启动时间对于许多应用的成功至关重要,我们正在不断推进 React Native 的边界。对于我们在 Hermes 中实现的任何新的 JavaScript 功能,我们都会仔细监控它们对生产性能的影响,并确保它们不会使指标倒退。在 Facebook,我们目前正在试验一个 Metro 中专用于 Hermes 的 Babel 转换配置文件,以使用 Hermes 的原生 ESNext 实现替换十几个 Babel 转换。我们能够在许多界面上观察到 18-25% 的 TTI 改进和 整体字节码大小的减小,我们期望在 OSS 中看到类似的结果。
除了启动性能之外,我们还发现内存占用是 React Native 应用(尤其是 虚拟现实)的一个改进机会。 感谢我们作为 JavaScript 引擎拥有的底层控制权,我们能够通过挤压比特和字节来提供多轮内存优化
- 以前,所有 JavaScript 值都表示为 64 位 NaN-boxing 编码的标记值,以表示 64 位架构上的浮点双精度数和指针。然而,这在实践中是浪费的,因为大多数数字都是小整数 (SMI),并且客户端应用程序的 JavaScript 堆通常预计不会大于 4GiB。为了解决这个问题,我们引入了一种新的 32 位编码,其中 SMI 和指针编码为 29 位(因为指针是 8 字节对齐的,我们可以假设底部 3 位始终为零),其余的 JS 数字被装箱到堆上。 这减少了约 30% 的 JavaScript 堆大小。
- 不同类型的 JavaScript 对象在 JavaScript 堆中表示为不同类型的 GC 管理单元。通过积极优化这些单元的标头的内存布局,我们能够将内存使用量再减少约 15%。
我们对 Hermes 的关键决策之一是不实现 即时 (JIT) 编译器,因为我们认为对于大多数 React Native 应用来说,额外的预热成本以及二进制文件和内存上的额外占用实际上是不值得的。多年来,我们投入了大量精力来优化解释器性能和编译器优化,以使 Hermes 的吞吐量在 React Native 工作负载方面与其他引擎相比具有竞争力。我们将继续专注于通过识别来自各个方面的性能瓶颈(解释器分发循环、堆栈布局、对象模型、GC 等)来提高吞吐量。期待在即将发布的版本中看到更多数据!
垂直整合方面的先锋
在 Facebook,我们更喜欢在一个大型 单体仓库 中共置项目。通过使引擎 (Hermes) 和主机 (React Native) 紧密迭代,我们为垂直整合开辟了很大的空间。 仅举几例
- Hermes 支持 使用 Chrome 调试器进行设备上的 JavaScript 调试,方法是使用 Chrome DevTools 协议。它比传统的“远程 JS 调试”(它使用应用内代理在桌面 Chrome 中运行 JS)更好,因为它支持调试同步原生调用并保证一致的运行时环境。 与 React DevTools、Metro、Inspector 等一起,Hermes 调试器现在是 Flipper 的一部分,以提供一站式开发者体验。
- 在 React Native 应用的初始化路径期间分配的对象通常是长寿命的,并且不遵循分代 GC 利用的分代假设。因此,我们 在 React Native 中配置了 Hermes,将前 32MiB 直接分配到老生代(称为预先老化),以避免触发 GC 暂停并延迟 TTI。
- 新的 React Native 架构很大程度上基于 JSI(或 JavaScript 接口),这是一个轻量级的通用 API,用于将 JavaScript 引擎嵌入到 C++ 程序中。通过让维护 JS 引擎的团队也维护 JSI API 实现,我们有信心提供最佳的集成,该集成在 Facebook 的规模上是可靠、高性能且经过实战考验的。
- 使 JavaScript 并发原语(例如 promise)和平台并发原语(例如 微任务)在语义上正确且性能良好,对于 React 并发渲染和 React Native 应用的未来至关重要。从历史上看,React Native 中的 promise 是使用非标准化的
setImmediate
API polyfill 的。我们正在努力使来自 JS 引擎的本机 promise 和微任务通过 JSI 可用,并在平台中引入queueMicrotask
,这是 Web 标准的最新添加,以更好地支持现代异步 JavaScript 代码。
团结整个社区
Hermes 对 Facebook 来说非常棒。但在我们的社区能够使用 Hermes 为整个生态系统中的体验提供动力,以便每个人都能利用其所有功能并拥抱其全部潜力之前,我们的工作尚未完成。
扩展到新平台
Hermes 最初仅针对 Android 上的 React Native 开源。从那时起,我们很高兴看到我们的社区成员将 Hermes 支持扩展到 React Native 生态系统已扩展到的许多其他平台。
Callstack 领导了将 Hermes 带到 React Native 0.64 中 iOS 平台 的工作。他们撰写了 一系列文章 并主持了一个 播客,讲述他们是如何实现这一目标的。根据他们的基准测试,与 iOS 上的 JSC 相比,Hermes 能够 持续提供约 40% 的启动改进和约 18% 的内存减少,而应用大小开销仅为 2.4 MiB。我鼓励你 亲眼见证它的实际效果。
Microsoft 一直在将 Hermes 带到 React Native for Windows 和 macOS。 在 Microsoft Build 2020 上,Microsoft 分享说,在 React Native for Windows 上,Hermes 的内存影响(工作集)比 Chakra 引擎低 13%。最近,在一些综合基准测试中,他们发现 Hermes 0.8(随 Hades 和上述 SMI 和指针压缩优化一起发布)比其他引擎使用的内存少 30%-40%。毫不奇怪,构建在 React Native 上的 桌面 Messenger 视频通话体验也由 Hermes 提供支持。
最后但并非最不重要的一点是,Hermes 也为 Oculus 上使用 React 技术系列构建的所有虚拟现实体验提供支持,包括 Oculus Home。
支持我们的社区
我们承认仍然存在一些阻碍社区部分成员采用 Hermes 的障碍,我们致力于为这些缺失的功能构建支持。 我们的目标是功能齐全,以便 Hermes 成为大多数 React Native 应用的正确选择。 以下是社区如何塑造 Hermes 路线图的
Proxy
和Reflect
最初被排除在 Hermes 之外,因为 Facebook 没有使用它们。 我们还担心,即使未使用 Proxy,添加 Proxy 也会损害属性查找性能。 但由于 MobX 和 Immer 等流行的库,Proxy 迅速成为 Hermes 最受用户要求的功能。 我们仔细评估并决定仅为社区构建它,并且我们设法以非常低的成本实现了它。 由于这是一个我们不使用的功能,因此我们依靠社区来证明其稳定性。 我们首先在标志后面测试 Proxy,并为 release v0.4 和 v0.5 创建了选择加入 npm 包,并且它 从 v0.7 开始默认启用。- ECMAScript 国际化 API 规范(ECMA-402 或
Intl
) 是 第二大最受用户要求的功能。Intl
是一组庞大的 API,通常需要实现包含 6MB 大小 的 Unicode CLDR 数据。 这就是为什么像 FormatJS(又名react-intl
) 这样的 polyfill 和像 社区 JSC 的国际变体构建 这样的 JS 引擎如此庞大的原因。 为了避免大幅增加 Hermes 的二进制文件大小,我们决定通过消耗和映射操作系统中包含的库提供的 ICU 功能来实现它,但代价是跨平台行为上存在一些(通常是微小的)差异。- Microsoft 合作构建了 Android 上的支持。 它几乎涵盖了从 ECMA-402 到 ES2020 的所有内容,尺寸影响仅小至 3%(每个 ABI 57-62K)。 我们在 Twitter 上发起了一项民意调查,结果强烈支持默认包含
Intl
,所以我们这样做了,并且它从 v0.8 版本 开始可用。 - Facebook 赞助了 Major League Hacking 以启动一个 远程开源 fellowship 项目。 去年,我们推出了 Hermes 采样分析器。 今年,我们的 fellows 将与来自 Hermes、React Native 和 Callstack 的成员合作,在 iOS 上为 Hermes
Intl
添加支持。 敬请关注!
- Microsoft 合作构建了 Android 上的支持。 它几乎涵盖了从 ECMA-402 到 ES2020 的所有内容,尺寸影响仅小至 3%(每个 ABI 57-62K)。 我们在 Twitter 上发起了一项民意调查,结果强烈支持默认包含
- 我们感谢人们与我们合作发现影响社区的问题。
- 人们帮助我们识别了关键的规范差异,例如
Array.prototype.sort
的稳定性,该稳定性在 ES2019 中进行了修订。 这已得到修复,并将在下一个版本中提供。 - 人们发现我们的默认堆大小限制太小,导致 不必要的 GC 压力 和 OOM 崩溃,许多不熟悉自定义 Hermes GC 配置的用户遇到了这些问题。 因此,我们将其从 512MiB 增加到 3GiB,使其默认情况下对于大多数用户来说都足够了。
- 人们还报告说,我们专门的
Function.prototype.toString
实现 导致在执行不正确的特性检测的库中性能下降,并且 阻止用户进行源代码注入。 这有助于我们加强我们的立场,即 Hermes 在可能的情况下不应妨碍开发者,并尊重事实上的实践。
- 人们帮助我们识别了关键的规范差异,例如
总结
总之,我们的愿景是使 Hermes 准备好成为所有 React Native 平台上的默认 JavaScript 引擎。 我们已经开始为此努力,我们希望听到大家对此方向的看法。
为生态系统做好平稳采用的准备对我们来说至关重要。 我们鼓励您试用 Hermes,并在我们的 GitHub 仓库 上提交有关任何反馈、问题、功能请求和不兼容性的 issue。
感谢
我们要感谢 Hermes 团队、React Native 团队以及 React Native 社区的众多贡献者为改进 Hermes 所做的工作。
我还要亲自感谢(按字母顺序排列)Eli White、Luna Wei、Neil Dhar、Tim Yung、Tzvetan Mikov 以及许多其他人在写作过程中的帮助。