迈向将 Hermes 设为默认引擎
本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →
自2019 年我们发布 Hermes 以来,它在社区中的采用率日益提高。Expo团队维护着流行的 React Native 应用元框架,最近在用户最期待的功能投票中胜出后,宣布了实验性支持Hermes。流行移动数据库Realm团队也于近期发布了 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 引擎必须"停止一切"(stop the world)执行垃圾回收(GC)时。
Hermes 原先的默认垃圾回收器 GenGC 是单线程分代垃圾回收器。新生代采用典型的半空间复制策略,老年代则使用标记-整理策略,能高效地将内存返还操作系统。但由于单线程特性,GenGC 存在引发长时间 GC 停顿的缺陷。在 Facebook for Android 等复杂应用中,我们观察到平均停顿时间为 200 毫秒,p99 达 1.4 秒。考虑到 Facebook for Android 庞大且多样的用户群,甚至出现过长达 7 秒的停顿。
为了缓解这一问题,我们实现了一个全新的准并发垃圾回收器 Hades。Hades 的新生代回收机制与 GenGC 完全相同,但其老年代采用基于初始快照的标记-清除回收策略,通过在后台线程执行大部分工作,显著减少了垃圾回收的暂停时间,避免阻塞执行 JavaScript 代码的主线程。我们的数据显示,在 64 位设备上 Hades 的 p99.9 暂停时间仅为 48ms(比 GenGC 快 34 倍!),在 32 位设备上(此时以单线程增量模式运行)p99.9 暂停时间约为 88ms。这些暂停时间的优化可能牺牲部分整体吞吐量,因为需要更昂贵的写屏障、基于空闲列表的较慢内存分配(而非指针碰撞分配器)以及堆碎片增加。但我们认为这是值得的权衡取舍,并通过内存合并和其他优化实现了更低的内存占用(后文将详述)。
攻克性能瓶颈点
应用启动时间对众多应用至关重要,我们正持续突破 React Native 的性能边界。对于 Hermes 实现的每个新 JavaScript 特性,我们都会严密监控生产环境性能影响,确保关键指标不倒退。在 Facebook,我们正试验为 Metro 中的 Hermes 定制 Babel 转换配置,用 Hermes 原生 ESNext 实现替代十余个 Babel 转换器。我们在多个产品界面观测到 TTI 提升 18-25%,字节码体积整体减小,预计开源社区也将获得类似收益。
除启动性能外,我们发现内存占用(尤其在虚拟现实场景)也是 React Native 应用的优化重点。得益于 JavaScript 引擎的底层控制能力,我们通过精打细算实现多轮内存优化:
-
此前所有 JavaScript 值在 64 位架构均采用 64 位 NaN-boxing 编码标签值表示浮点双精度数和指针。但实践中这存在浪费——多数数字是小整数(SMI),且客户端应用的 JavaScript 堆通常不会超过 4GiB。为此我们引入 32 位编码:SMI 和指针以 29 位存储(因指针 8 字节对齐,末三位恒为零),其余数值装箱存入堆内存。此举使 JavaScript 堆内存减少约 30%。
-
不同类型 JavaScript 对象在堆中对应不同 GC 管理单元。通过极致优化这些单元头部的内存布局,我们再度降低约 15% 内存占用。
Hermes 的关键决策之一是不实现即时编译(JIT),因为我们相信对多数 React Native 应用而言,其额外的预热开销及二进制体积/内存占用并不划算。多年来我们持续优化解释器性能和编译策略,使 Hermes 在 React Native 工作负载中的吞吐量媲美其他引擎。我们将继续从各环节(解释器调度循环、栈布局、对象模型、GC 等)识别性能瓶颈以提升吞吐量——敬请期待后续版本的数据!
垂直整合的创新实践
在 Facebook,我们倾向将项目集中在大型单体仓库中。通过让引擎(Hermes)与宿主环境(React Native)紧密协同迭代,我们开辟了大量垂直整合空间,例如:
-
Hermes 支持通过 Chrome 开发者工具协议实现设备端 JavaScript 调试。这比传统的"远程 JS 调试"(通过应用内代理在桌面 Chrome 中运行 JS)更优越,因为它支持调试同步原生调用并确保运行时环境的一致性。与 React DevTools、Metro、Inspector 等工具共同集成后,Hermes 调试器现已成为 Flipper 的一部分,提供一站式开发体验。
-
React Native 应用初始化过程中分配的对象通常具有长生命周期,不符合分代垃圾回收器依赖的_代际假说_。因此我们在 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 曾通过非标准的
setImmediateAPI 进行填充实现。我们正致力于通过 JSI 暴露 JS 引擎的原生 Promise 和微任务,并向平台引入近期加入 Web 标准的queueMicrotask,以更好地支持现代异步 JavaScript 代码。
携手整个社区共同前行
Hermes 在 Facebook 内部表现出色,但我们的使命尚未完成——只有当整个生态都能利用 Hermes 驱动产品体验,让所有开发者充分运用其特性并释放全部潜力时,才算是真正的成功。
拓展至新平台
Hermes 最初仅面向 Android 平台的 React Native 开源。此后我们欣喜地看到社区成员将其支持范围扩展到 React Native 生态已覆盖的众多平台。
Callstack 主导了 React Native 0.64 版本中 iOS 平台 Hermes 支持的实现工作。他们撰写了系列文章并制作了播客节目详述实现过程。根据其基准测试,在 Mattermost 应用中,Hermes 相比 JSC 在 iOS 平台上持续带来约 40% 的启动性能提升和 18% 的内存减少,仅增加 2.4 MiB 的应用体积开销。欢迎您亲身体验效果展示。
微软一直在将 Hermes 引入 React Native for Windows 和 macOS。在 Microsoft Build 2020 大会上,微软分享道,在 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 最受期待的功能。经过仔细评估,我们决定专为社区实现此功能,并以极低的成本完成了开发。由于这是我们自身不会使用的功能,我们依赖社区来验证其稳定性。我们最初通过标志控制进行测试,并为 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 功能并做平台适配。尽管这会导致跨平台行为存在轻微差异,但微软团队已协作完成 Android 端的实现:- 该方案覆盖了 ECMA-402 至 ES2020 的几乎所有规范,体积增幅仅 3%(每 ABI 架构增加 57-62K)。我们在 Twitter 发起投票调研,结果显示强烈支持默认集成
Intl。因此从 v0.8 版本起,该功能已正式启用。 - Facebook 已赞助 Major League Hacking 推出远程开源奖学金计划。继去年发布 Hermes 采样分析器后,本届学员将与 Hermes、React Native 及 Callstack 成员协作,为 iOS 平台实现 Hermes
Intl支持。敬请期待!
- 该方案覆盖了 ECMA-402 至 ES2020 的几乎所有规范,体积增幅仅 3%(每 ABI 架构增加 57-62K)。我们在 Twitter 发起投票调研,结果显示强烈支持默认集成
-
我们衷心感谢社区成员与我们共同发现影响开发者的问题:
- 用户协助我们识别了关键规范偏差,例如
Array.prototype.sort的稳定性问题(在 ES2019 中修订)。该问题已修复,将在下个版本发布。 - 社区反馈默认堆大小限制过小,导致不熟悉 Hermes GC 配置的用户面临不必要的 GC 压力和 OOM 崩溃。为此我们将默认值从 512MiB 提升至 3GiB,满足绝大多数场景需求。
- 用户报告我们优化的
Function.prototype.toString实现导致某些库进行不规范特性检测时性能下降,并阻碍源代码注入操作。这些反馈坚定了我们的立场:Hermes 应尽可能避免干扰开发者,并尊重行业实践。
- 用户协助我们识别了关键规范偏差,例如
总结
总而言之,我们的愿景是让 Hermes 成为所有 React Native 平台的默认 JavaScript 引擎。相关工作已启动,期待听到大家对此方向的反馈。
为生态平稳过渡做好准备至关重要。我们鼓励您试用 Hermes,并在 GitHub 仓库提交问题报告,包括反馈、疑问、功能请求及兼容性问题。
致谢
衷心感谢 Hermes 团队、React Native 团队以及众多 React Native 社区贡献者对 Hermes 的持续改进。
我谨个人致谢(按字母排序):Eli White、Luna Wei、Neil Dhar、Tim Yung、Tzvetan Mikov 等成员在本文撰写过程中的鼎力支持。
