跳到主要内容

Hermes 迈向默认引擎

·14 分钟阅读
Xuan Huang
黄轩
Meta 软件工程师

2019 年我们发布 Hermes 以来,它在社区中的采用率不断提高。维护着 React Native 应用程序流行元框架的 Expo 团队,最近在 Hermes 成为 Expo 最受请求的功能之一后,宣布了实验性 支持。流行的移动数据库 Realm 团队也于最近发布了对 Hermes 的 alpha 支持。在这篇文章中,我们想强调过去两年中我们为推动 Hermes 成为 React Native **最佳** JavaScript 引擎所取得的一些最令人兴奋的进展。展望未来,我们相信通过这些改进以及将来的更多改进,我们可以让 Hermes 成为 React Native 所有平台上的默认 JavaScript 引擎。

为 React Native 进行优化

Hermes 的一个显著特点是它提前执行编译工作,这意味着启用 Hermes 的 React Native 应用程序会附带预编译的优化字节码,而不是纯 JavaScript 源代码。这大大减少了为用户启动产品所需的工作量。来自 Facebook 和社区应用程序的测量结果表明,启用 Hermes 通常会使产品的 TTI(或 可交互时间)指标减少近一半。

话虽如此,我们一直在改进 Hermes 的许多其他方面,使其作为专用于 React Native 的 JavaScript 引擎变得更好。

为 Fabric 构建新的垃圾回收器

随着新 React Native 架构中即将推出的 Fabric 渲染器,将有可能在 UI 线程上同步调用 JavaScript。但是,这意味着如果 JavaScript 线程执行时间过长,可能会导致明显的 UI 帧丢失并阻塞用户输入。React Fiber 启用的 并发渲染 将通过将渲染工作分成块来避免调度长时间的 JavaScript 任务。然而,JavaScript 线程还有另一个常见的延迟来源——当 JavaScript 引擎必须“停止世界”来执行垃圾回收 (GC) 时。

Hermes 中之前的默认垃圾回收器 GenGC 是一种单线程分代垃圾回收器。新代使用典型的半空间复制策略,旧代使用标记-整理策略,使其非常擅长积极地将内存返回给操作系统。由于其单线程,GenGC 的缺点是会导致长时间的 GC 暂停。在像 Facebook for Android 这样复杂的应用程序上,我们观察到平均暂停时间为 200 毫秒,在 p99 时为 1.4 秒。考虑到 Facebook for Android 庞大而多样化的用户群,我们甚至看到它长达 7 秒。

为了缓解这个问题,我们实现了一个全新的 **主要并发** GC,名为 Hades。Hades 对其年轻代的收集方式与 GenGC 完全相同,但它通过快照式标记-清除回收器管理其老年代。它可以在不阻塞引擎主线程执行 JavaScript 代码的情况下,在后台线程中执行大部分工作,从而显著减少 GC 暂停时间。**我们的统计数据显示,Hades 在 64 位设备上的 p99.9 暂停时间仅为 48 毫秒(比 GenGC 快 34 倍!)**,在 32 位设备上(它作为单线程**增量** GC 运行)p99.9 暂停时间约为 88 毫秒。这些暂停时间的改进可能会以总体吞吐量为代价,因为需要更昂贵的写入屏障、基于空闲列表的分配速度较慢(与碰撞指针分配器相比)以及堆碎片化增加。我们认为这些是正确的权衡,并且我们通过合并和我们将讨论的其他内存优化实现了总体上更低的内存消耗。

解决性能痛点

应用程序的启动时间对于许多应用程序的成功至关重要,我们正在不断突破 React Native 的极限。对于我们在 Hermes 中实现的任何新 JavaScript 功能,我们都会仔细监控它们对生产性能的影响,并确保它们不会降低指标。在 Facebook,我们目前正在尝试在 Metro 中为 Hermes 提供一个专用的 Babel 转换配置文件,以用 Hermes 的原生 ESNext 实现替换十几个 Babel 转换。我们观察到许多界面的 **TTI 提高了 18-25%**,**整体字节码大小减小**,我们预计开源也会出现类似的结果。

除了启动性能,我们还将内存占用确定为 React Native 应用程序的改进机会,特别是对于 虚拟现实。由于我们作为 JavaScript 引擎拥有的底层控制,我们能够通过挤出位数和字节来实现多轮内存优化。

  1. 以前,所有 JavaScript 值都表示为 64 位 NaN-boxing 编码的标记值,以表示 64 位体系结构上的浮点双精度和指针。然而,这在实践中是浪费的,因为大多数数字是小整数(SMI),并且客户端应用程序的 JavaScript 堆通常不会大于 4GiB。为了解决这个问题,我们引入了一种新的 32 位编码,其中 SMI 和指针以 29 位编码(因为指针是 8 字节对齐的,我们可以假设最低 3 位始终为零),其余的 JS 数字则装箱到堆中。**这使 JavaScript 堆大小减少了约 30%。**
  2. 不同类型的 JavaScript 对象在 JavaScript 堆中表示为不同类型的 GC 管理单元。通过积极优化这些单元头的内存布局,**我们能够将内存使用量再减少约 15%**。

我们对 Hermes 做出的一个关键决定是不实现 即时 (JIT) 编译器,因为我们相信对于大多数 React Native 应用程序而言,额外的预热成本以及二进制和内存的额外占用实际上是不值得的。多年来,我们投入了大量精力优化解释器性能和编译器优化,以使 Hermes 的吞吐量在 React Native 工作负载方面与 D 其他引擎具有竞争力。我们将继续通过识别各处的性能瓶颈(解释器调度循环、堆栈布局、对象模型、GC 等)来专注于提高吞吐量。敬请期待即将发布的版本中更多数据!

垂直整合的先锋

在 Facebook,我们更喜欢将项目放在一个大型 monorepo 中。通过让引擎 (Hermes) 和宿主 (React Native) 紧密协作,我们为垂直整合创造了大量空间。举几个例子:

  • Hermes 通过使用 Chrome DevTools Protocol 支持 使用 Chrome 调试器进行设备端 JavaScript 调试。它优于传统的“远程 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 并发原语(例如 promises)和平台并发原语(例如 微任务)在语义上正确且性能良好,对于 React 并发渲染和 React Native 应用程序的未来至关重要。历史上,React Native 中的 promises 使用非标准化的 setImmediate API 进行 polyfill。我们正在努力通过 JSI 提供 JS 引擎的原生 promises 和微任务,并引入 queueMicrotask,这是 Web 标准的最新补充,以更好地支持现代异步 JavaScript 代码。

与整个社区同行

Hermes 在 Facebook 对我们来说一直非常棒。但我们的工作尚未完成,直到我们的社区能够使用 Hermes 来支持整个生态系统中的体验,这样每个人都可以利用其所有功能并发挥其全部潜力。

拓展到新平台

Hermes 最初仅作为 React Native on Android 的开源项目。此后,我们很高兴看到社区成员将 Hermes 支持扩展到 React Native 生态系统已扩展的许多其他平台

Callstack 牵头将 Hermes 引入 React Native 0.64 的 iOS 版本。他们撰写了 一系列文章 并主持了一个 播客,讲述了他们是如何实现的。根据他们的基准测试,与 JSC 相比,Hermes 能够为 Mattermost 应用程序**在 iOS 上持续提供约 40% 的启动改进和约 18% 的内存减少**,而应用程序大小开销仅为 2.4 MiB。我鼓励您 亲眼看看

微软一直在将 Hermes 引入 React Native for Windows 和 macOS在 2020 年的 Microsoft Build 大会上,微软分享道,Hermes 的内存影响(工作集)比 React Native for Windows 上的 Chakra 引擎低 13%。最近,在一些综合基准测试中,他们发现 Hermes 0.8(随 Hades 和上述 SMI 以及指针压缩优化一起发布)**比其他引擎使用的内存减少 30%-40%**。毫不奇怪,基于 React Native 构建的 桌面 Messenger 视频通话体验也由 Hermes 提供支持。

最后但同样重要的是,Hermes 也一直在为 Oculus 上所有使用 React 技术家族构建的虚拟现实体验提供支持,包括 Oculus Home。

支持我们的社区

我们承认仍然存在阻碍社区部分成员采用 Hermes 的障碍,我们致力于为这些缺失的功能构建支持。我们的目标是功能齐全,以便 Hermes 成为大多数 React Native 应用程序的正确选择。以下是社区如何塑造 Hermes 路线图的例子:

总结

总而言之,我们的愿景是让 Hermes 准备好成为所有 React Native 平台上的默认 JavaScript 引擎。我们已经开始朝着这个方向努力,我们希望听到大家对此方向的意见。

为生态系统做好平稳过渡的准备对我们来说至关重要。我们鼓励您尝试 Hermes,并在我们的 GitHub 仓库 上提交任何反馈、问题、功能请求和不兼容性。

致谢

我们要感谢 Hermes 团队、React Native 团队以及 React Native 社区的众多贡献者为改进 Hermes 所做的工作。

我还想特别感谢(按字母顺序排列)Eli White、Luna Wei、Neil Dhar、Tim Yung、Tzvetan Mikov 以及许多其他人在撰写本文期间提供的帮助。