前前后后花了三个多月,把 Hermes 从“听说过”折腾到能在自己项目里真正落地,踩了不少坑,也绕了很多弯路。回头看这几个月,真正让我搞懂 Hermes、让项目实实在在变快的,其实不是那些源码分析文章,也不是各种性能对比帖,而是四个非常基础的问题。如果你刚开始接触 Hermes,建议先把这四件事学扎实,其余的都可以先放一放。
先说清楚,这里聊的是 React Native 生态里的 Hermes 引擎,它不是某个独立框架,而是负责在移动端解释和执行 JavaScript 的那一层。这篇文章适合正在用 React Native 做 App、想通过切换引擎来改善启动速度和内存占用,但对 Hermes 还处于“听过名字、不知道怎么学起”状态的开发者。
我把我走过的弯路、最后总结下来的关键路径,一次性写清楚。
1. 先吃透“Hermes 靠预编译字节码提速”这条主线
接触 Hermes 时,我最先犯的错就是去找配置开关,把hermesEnabled=true打开,重新打包,然后盯着数字看有没有变快。这样做不能说错,但很容易给人一种“开完了也就那样”的错觉。因为漏掉了最重要的一条逻辑主线:Hermes 比原有方案快,不是因为它是“另一款 JS 引擎”,而是因为它把 JS 代码的处理时机整体往前挪了一大截。
1.1 JSC 是现场备菜,Hermes 是中央厨房
在 React Native 的传统模式下,JavaScript 代码交给 JavaScriptCore(JSC)执行。你要理解 JSC 的工作流:App 启动后会加载那整包 JS 代码,先做词法分析和语法解析,生成抽象语法树,再编译成字节码,然后边解释执行边用 JIT 对热点函数做优化。也就是说,从加载到首屏可交互之前,这个“解析 + 编译”的过程必须现场完成,很吃 CPU,也很吃内存。
Hermes 换了一种思路:把解析和编译这两步直接放到打包构建阶段完成,构建时就把 JS 源码编译成字节码,也就是 HBC(Hermes Bytecode)。App 运行时拿到的不再是源代码,而是一份已经处理过的二进制形态产物,直接交给 Hermes 虚拟机执行。
打个比方:JSC 像一间没有预制菜的餐厅,你点菜后厨师才开始备菜、切菜、炒菜,等菜时间取决于厨房当天的忙碌程度;Hermes 则是中央厨房提前把菜做成标准化的半成品或预制菜,门店收到后只需要快速加热上桌。对移动端这种资源有限的环境来说,启动阶段少干活就是最大的优化。
1.2 字节码不是机器码,但它改变了执行起点
有人会问,字节码到底是个什么东西?它是介于“源代码”和“机器码”之间的一种中间表示。HBC 不能脱离 Hermes 引擎直接跑在芯片上,但它是给 Hermes 虚拟机量身定制的,加载、解析、执行的速度都比面向通用场景的 JS 源码处理流程快得多。
实际上 Hermes 也没少在编译器层面做文章,包括面向移动端优化的优化器、对内存更友好的 GC 策略、以及刻意控制的指令集规模。不过这些在入门阶段不需要深究,你只需要建立两个认知:
- HBC 产物通常比原始 JS 源码更紧凑,这是包体收益的来源之一。
- JS 执行链路上的“解析和编译”不再占用用户可感知的等待时间。
我在实际学习中还有一个体会:带着“预编译”这条主线去读文档,很多看似玄乎的概念都能串起来。比如为什么官方说内存足迹更加稳定,因为运行时不需要维护 JIT 那套中间层缓存;为什么某些动态能力受限,因为预编译模型天然排斥运行时常量字符串的即时编译执行。
1.3 这条主线决定了后面所有操作的方向
“为什么必须用 Release 构建来测性能”,是因为 Debug 模式下为了支持调试器,Hermes 会保留更多的辅助信息,很多优化路径并不生效;“为什么某些第三方库在 Hermes 下报错”,大概率是库依赖了动态执行能力;“为什么开了 Hermes 启动还是慢”,那就要回到你自己的业务链路里找瓶颈,而不是怪引擎。这条主线会反复出现在你之后的每一次排查和决策中。
建议你做完的第一件事,不是去背文档,而是搭一个最小工程,分别用关掉和打开 Hermes 的 Release 包跑真机,量一下冷启动耗时。你不需要得到一个“绝对更快”的结论,而是用自己的设备去验证“少干活”这个概念到底体现在哪里。跑完这两轮对比,你再回来看文档里的 Architecture 篇幅,思路会清晰很多。
2. 配置 Hermes 的准确姿势:两个开关和一条版本链
如果把 Hermes 入门分成“理解”和“落地”两部分,理解是主线,落地则是配置。配置本身不复杂,真正麻烦的是版本匹配。我见过太多人卡在各种奇怪的编译报错上,兜兜转转很久,最后发现根因只是版本链没对齐。
2.1 最基础的开关,Android 与 iOS 各一行
首先明确一个事实:较新版本的 React Native 脚手架已经在创建工程时默认开启 Hermes。如果你用的是新工程,大概率什么都不用改。但如果你是把旧工程迁移过来,或者需要显式控制开关,配置就这么几行。
Android 侧在android/gradle.properties文件里:
hermesEnabled=trueiOS 侧在ios/Podfile里:
hermes_enabled true改完之后,Android 重新执行一次构建,iOS 执行pod install再构建。这里想提醒一点:iOS 侧修改后不要偷懒跳过pod install,不然原生工程里根本不会链接新的 Hermes 依赖。
2.2 版本链是最大的暗坑,Debug 正常不代表 Release 没问题
Hermes 并不是一个可以独立升级的组件,它和 React Native 的版本绑定很深。RN 升级时,对应的 Hermes 版本、原生 C++ 接口、字节码格式都可能发生变化。你在 Podfile 里锁了某个旧版本的 Hermes,但 RN 本体已经升上去,这种“混搭”很可能编译期不报错,运行期才出问题。
我在排查某个业务 App 的 Release 闪退问题时就经历过一次。Debug 包一直正常,Release 包一打开就崩,一开始完全猜不到是引擎版本问题,一层层剥掉业务代码、用只包含空页面的分支做二分对比之后,才定位到 Podfile 里锁了一个与当前 RN 版本不匹配的 Hermes 依赖。整个过程花了两个工作日,期间看着各种异常堆栈都是原生层的崩溃符号,完全没法直接对应到业务代码。
那次经历之后,我的规则变得非常简单:凡是升级 RN 版本,必须同步执行一次pod install或gradle sync,并且留意依赖树里 Hermes 的版本是否随之更新。构建日志里出现关于 Hermes 的警告信息时,不要直接无视,其中相当一部分就是在提示“版本不兼容”。
为什么强调“Debug 正常不代表 Release 没问题”?因为 Hermes 的 Debug 构建和 Release 构建走的是不同的执行路径。Debug 构建为了调试体验会保留额外信息,某些问题只在 Release 的字节码执行路径上触发。所以判断一个引擎配置改对了没有,一定要以 Release 构建结果为准。
2.3 常见报错的排查方向
在实际社群讨论中,关于 Hermes 的报错其实高度集中,我把它们整理成一个排查方向表,遇到问题可以先对照一下,而不是一头扎进源码。
| 现象 | 常见根因 | 排查方向 |
|---|---|---|
| Debug 正常,Release 启动闪退 | Hermes 版本与 RN 版本不匹配 | 检查 Podfile 锁文件与依赖树,重装依赖 |
| 构建时提示找不到 Hermes 头文件或静态库 | 原生依赖未同步 | Android 检查 Gradle 同步,iOS 检查 pod install |
| 某个第三方库运行时报“引擎或环境不支持” | 库依赖动态执行或特定引擎能力 | 搜索该库的 Issue 列表里的 Hermes 关键字,找替代方案 |
| 打开 Hermes 后首次打包特别慢 | 构建期新增了 JS 到 HBC 的编译步骤 | 属于正常现象,关注增量构建体验即可 |
这里想分享一个核心观点:与其去背 Hermes 的每一个配置项,不如掌握“RN 版本决定 Hermes 版本 → Hermes 版本决定字节码格式 → 字节码格式决定 Release 运行行为”这条链路。你只要把这条链路上每个环节在哪个文件、哪个命令里能看到弄清楚,遇到问题时的排查路径就是清晰的,不会两眼一抹黑。
3. 会用 Hermes 的调试与测量,再谈优化
很多人配置完 Hermes 就急着去看优化效果,结果拿 Debug 包一顿测,数据忽上忽下,最后得出一堆没有参考价值的结论。调试和测量工具的使用本身就有一套独立的逻辑,尤其是 Hermes 这种 Debug 和 Release 行为差异很大的引擎,如果你不把工具用对,后面的“优化”都是建立在幻觉之上的。
3.1 Debug 环境不能替你回答“快不快”
Hermes 是支持调试的。React Native 的开发者工具可以通过 Chrome DevTools 协议和 Hermes 连接,打断点、看 console、查看作用域变量都可以,体验和平时调试 JS 差不多。但同时你要清楚,调试模式下的 Hermes 并不等于优化模式下的 Hermes。
Debug 模式下,Hermes 会关闭或弱化一批优化手段,以便提供完整的可观测性;而且调试器和引擎之间还会多一层通信开销。所以你在 Debug 包上看到的时间、内存数据,只能作为功能与流程是否正确的大致参考,不能作为性能结论。任何关于启动耗时、内存占用的结论,都应该在 Release 包上获取。
这个认知我是在一次性能对比中才彻底建立的。当时用同一个页面切换开关测了十几轮,Debug 数据忽快忽慢,看不出规律;换到 Release 包之后数据才稳定下来,Hermes 的启动收益才开始“肉眼可见”。后来我把这条规则写进团队的性能测试流程里:所有引擎相关指标只认 Release 构建。
3.2 上手成本最低的三个工具
有了正确的环境意识之后,还需要几个趁手工具。Hermes 入门阶段最值得先掌握的就是下面三个,其他花哨的功能可以先不碰。
第一,Sampling Profiler,用于分析 JS 线程的 CPU 热点。在开发者菜单里开启采样,正常操作复现卡顿路径,结束后导出 profile 文件,放到浏览器的性能分析面板里看火焰图。注意采样周期不要太长,一般 10 秒至 1 分钟内足够,时间太长文件大且读数反而难分析。
第二,Heap Snapshot,用于内存与泄漏排查。连接 DevTools 后,在 Memory 面板里可以直接对 Hermes 的 JS 堆打快照,四五个快照做一次对比,定位“只增不减”的对象。这个工具在排查业务层内存泄漏时非常管用,比你在代码里到处打日志找泄漏点高效得多。
第三,Source Map 的收集与上传。这个不完全算 Hermes 专属,但开着 Hermes 后常被忽略。Release 包在打包时会生成对应的 Source Map,建议把它作为构建产物自动保存下来,并上传到你使用的崩溃监控平台。没有 Source Map,线上报错栈就是一串压缩后的乱码,无法对应到具体业务代码;有它之后才能把错误还原到项目里的某一行,排查线上问题快很多。
3.3 一次内存泄漏排查的完整链路
挑一个我复盘过很多次的内存泄漏排查路径,完整走一遍给你看,这也是 Hermes 工具链最典型的应用场景。
背景是某个业务页面反复进入退出后,App 的整体内存持续上涨。我的排查步骤是这样的:
- 在 Release 包中冷启动 App,进入目标页面完成后,立刻用调试器打一份 Heap Snapshot 作为基线。
- 连续进入并退出目标页面 10 到 15 次,模拟真实用户的高频操作。
- 等待至少 30 秒,让引擎有机会执行 GC,然后再打一份快照。
- 在快照对比视图中按构造函数统计实例数量变化,筛选出“实例数量明显增加,且在 GC 后依然没有回落”的对象。
- 顺着对象的引用链往上找,看是哪个业务对象还在持有它。
这套流程最容易被误导的地方,发生在第 3 步。如果你一直用 Debug 包测试,调试器本身的连接和快照操作会临时持有一些 JS 对象,很容易伪造出“好像有泄漏”的假象,实际是调试器的引用干扰了判定。我在一次持续了一下午的排查中,反复定位到某个列表项对象“泄漏”,最后把 Debug 包换成 Release 包再测,同样的操作路径下根本不存在泄漏,真正的泄漏点反而是某一个定时器回调查没有被及时清理。
所以再强调一次:调试和测量工具链是通的,但任何结论都要回到 Release 环境下做最终确认。这也是 Hermes 调试体系里最不值得交的学费。
4. 把“开了 Hermes”升级为“真正落地”:字节码、包体与验收
配置开关选对了,不等于 Hermes 就已经在你的项目里真正生效。很多人开着 Hermes,Release 包也正常跑,但从来没验证过产物到底是不是字节码,性能收益也没有形成一套可重复的测试方法。这一节要讲的就是“怎么让 Hermes 落地成可以被验证的结果”。
4.1 先确认 Release 里跑的真是字节码
有一个很反直觉的事实:你打开了 Hermes 配置,构建也没有报错,但产物可能根本没有经过 Hermes 的完整编译链路。尤其是从旧工程迁移、或者项目里的打包脚本经过大量定制的时候,这种情况并不少见。
验证方式不复杂,但需要你动手解包。Android Release 包构建完成后,把 APK 解压,找到assets/index.android.bundle这个文件,用文本编辑器打开看内容。如果里面是一大段可读的 JavaScript 代码,说明这个 bundle 根本没有被编译成字节码,Hermes 的优化没有真正生效;如果打开基本都是二进制乱码、只有少量可识别的文件头信息,那多半就是已经变成了 HBC 产物。
另一个佐证方式是看构建日志里有没有 Hermes 编译器相关步骤。Android 构建时如果 Hermes 生效,日志里会出现把 JS bundle 转换为字节码的环节;iOS 侧同理。如果日志里完全看不到相关步骤,优先检查配置是否真的传到了原生构建流程里,而不是只改了个文件就没重新构建。
4.2 用项目自己的数据验证收益
验证 Hermes 到底带来多少收益,最怕的不是没有数据,而是拿别人跑的基准数据套到自己的项目上。不同设备的 CPU 性能、不同业务的 JS 复杂度、不同版本的 RN,结果差异可以非常大。形成一个属于自己项目的测量方法,比看任何 Benchmark 都重要。
冷启动耗时建议用这条链路:
- Android 上,用
adb shell am start -W拿到总启动耗时,多跑几次取中位数,避免单次数据波动。 - iOS 上,使用系统自带的启动性能测试模板记录冷启动阶段耗时。
- 内存指标,关注 JS 侧 Heap 和进程常驻内存两个维度,前后台切换后再取样,连续测几轮看趋势。
测量时的对照组也要做严谨:同一台设备、同一个 RN 版本、同一个构建命令,只切换 Hermes 开关。这里的核心不是“快了多少”,而是看收益是否稳定、是否值得后续围绕它做更多优化。我在自己的项目里测下来,Hermes 对启动阶段 JS 解析时间的削减很明显,但如果你项目的主要瓶颈是网络请求串行或原生视图过多,Hermes 帮不了太多——它优化的是 JS 执行链路,不是整个 App 的全局性能。
4.3 不要迷信:该回退时别硬撑
和 Hermes 相关的限制,我认为最核心的一点是:预编译模型对“动态”并不友好。如果你的业务里大量依赖运行时构造代码字符串再去执行,或者在接入了依赖eval、new Function类能力的第三方库,Hermes 下大概率会出现性能问题甚至运行异常。另外还有一些库依赖特定引擎才存在的行为,也需要逐个排查和替换。
遇到这种情况,比较实际的处理方式:先看有没有功能替代库,再评估业务改造成本。如果确实有一两个核心库在 Hermes 下无法正常工作,暂时关掉 Hermes 也是一种合理决策。工具是为项目服务的,不存在“用了就一定高级”的说法。只要你的评估过程是基于项目里的真实数据和代码,选型结论就是站得住的。
5. 我花时间绕过的三个弯路,帮你踩平一下
开头说了,我用了三个多月,其中不少时间是在走弯路。这里把最有代表性的三个写出来,对标着一看就知道该不该在这件事上继续花时间。
5.1 别一上来就啃 C++ 源码
编程圈有个奇怪的习惯,觉得不进入源码层面就等于没学透。我不否认读 Hermes 的源码能让你对引擎的理解进入另一个层次,但那是针对“我要参与引擎定制、我要做底层性能剖析”的场景的。普通业务开发者入门阶段去啃源码,投入产出比非常低。
源码解决的是“引擎内部为什么这么设计”的问题,而你在实际项目里遇到的绝大多数问题,是“版本为什么对不上”“这个库为什么不兼容”“这个性能数据为什么异常”,这些问题从配置链路和调试工具就能获得答案,不需要先通读一遍解释器实现。等你真的遇到只有源码才能解释的现象,再带着问题去读,效率会高非常多。
5.2 别拿别人的 Benchmark 当圣旨
这一条我特别想强调。刷到的 Hermes 对比帖、跑分图,不能说没有参考价值,但它们只能告诉你“在一个特定设备、特定项目、特定构建参数下,Hermes 表现如何”,不能等价于你的项目也是这样。
我见过有人看到一个“快 40%”的数据之后兴冲冲开了 Hermes,结果自己项目里收益不足 10%,于是得出“Hermes 不行”的结论。也见过有人看到“包体减少 30%”后预期拉满,实测后发现原生层体积比想象中大,又开始怀疑是不是哪里配错了。实际上这些差异基本都来自项目自身的结构差异,和引擎本身没什么关系。正确做法是用上一节说的对照方法,在自己的工程里跑出一组可信数据,让数据替你拿主意。
5.3 别过早装一堆监控与分析全家桶
入门阶段好奇心旺盛,容易在项目里把所有能看到的性能监控、错误统计、上报库都装一遍。但这种做法带来的副作用是:大量监控代码本身也在消耗启动时间和内存,而且多套工具的采集口径不一致,反而让你没法判断 Hermes 到底改善了什么。
合理路径是先选一个最关心的指标,比如冷启动耗时或首屏内存峰值,先围绕这个指标把工具链跑通,形成“修改配置 → 测量 → 看数据 → 再调整”的闭环。一个闭环跑通之后,再逐步扩展其他指标的观测。这样做,每一条结论的来源都是清晰的,不会被一堆互相矛盾的数字淹没。
最后说一句我这几个月实践下来最深的体会:学这种带底层性质的引擎,一定不要被“进阶技巧”晃花了眼。先抓住预编译字节码这条主线,再建立自己的验证方法,遇到问题时按版本链和工具链去排查,你在 Hermes 上踩坑的概率就能降到很低。这四个基础点哪怕只用会了一半,也比把时间花在盲目折腾强得多。