Hermes与JSC深度对比:React Native引擎选型与性能优化指南
做 React Native 开发的朋友,面试时大概率都被问过这个问题:Hermes 和 JSC 有什么区别?我在准备“碎片八股文 #005”的时候把它翻来覆去啃了几遍,发现大多数人只答得出“Hermes 是 Meta 做的、JSC 是苹果的”这一层,再往下问就卡住了。其实这两个引擎从设计哲学到运行机制,基本上是两条相反的路线:一个为了浏览器而生,一个为了移动 App 而生。搞清楚它们的差异,不光面试能加分,实际做技术选型时也能少踩很多坑。
这篇文章我把两个引擎的出生背景、运行原理、内存管理、调试体验、切换实操、面试常见追问全部串一遍,适合准备面试的 RN 开发者,也适合正在纠结要不要在项目里开启 Hermes 的研发同学。文末还会附上我自己的实测经验,都是网上文档里不会写的那种。
1. 先搞清楚:这两个引擎到底是什么
很多八股文上来就列对比表,但如果不理解两者的“出身”,背下来的内容很快会忘。我先花点篇幅把两个引擎的定位讲透,之后再谈技术细节就顺理成章了。
1.1 JSC 的“家世”:苹果家的浏览器内核大脑
JSC,全称 JavaScriptCore,是 WebKit 浏览器引擎的 JavaScript 部分。WebKit 是苹果开发的开源浏览器内核,Safari 用的就是它,而 JSC 就是负责解析、编译、执行 JavaScript 的那颗“大脑”。
这里有一个关键背景要知道:React Native 早期选择 JSC 作为默认引擎,完全是“近水楼台先得月”。因为 iOS 上系统自带 JavaScriptCore.framework,RN 可以直接调它来执行 JS 逻辑,不需要额外打包一个 JS 引擎进 App。对当时刚起步的 RN 来说,这是成本最低、兼容性最好的方案。后来 Android 上通过 jsc-android 这个包来提供对应版本的 JSC 实现,但 JSC 的底层设计仍然围绕浏览器场景展开。
JSC 在浏览器里要跑重交互、重计算、大量 DOM 操作的页面,所以它走的是“极致执行效率”路线,核心手段就是 JIT(Just In Time,即时编译)技术。JSC 会先快速解释执行代码,同时收集热点函数信息,当某段代码被反复执行,就把它编译成机器码,从而获得接近原生的运行速度。这种设计在 Web 上非常成功,但到了移动 App 场景,问题也随之而来,后面我会详细讲。
1.2 Hermes 的诞生:Meta 为 React Native 量身打造的引擎
Hermes 是 Meta 在 2019 年开源的一个 JavaScript 引擎,但和 JSC 的“先有浏览器、后有 RN”不同,Hermes 从第一天起就是专门为 React Native 设计的。Meta 内部有着大量低端 Android 设备的用户,他们发现 JSC 在低端机上的表现很差:启动慢、内存占用高、页面卡顿明显。
于是 Meta 做了一款“反其道而行之”的引擎。它的目标不是让 JavaScript 跑得尽可能快,而是让 App 启动更快、内存占用更低、包体积更小。为此,Hermes 抛弃了 JIT 路线,改用 AOT(Ahead of Time,预先编译)模式:在打包阶段就把 JS 源码编译成字节码,App 运行时直接加载执行字节码,省去了 JIT 所需的预热时间。
简单类比一下:JSC 像是一个“现场做菜的厨师”,食材(JS 源码)到了之后现场洗、切、炒,虽然能根据顾客口味调整火候,但出餐时间不稳定;Hermes 则像“中央厨房送的预制菜”,在配送前就做好了,App 拿到手只需要简单加热就能上桌。对于移动端这种追求稳定启动速度的场景,Hermes 的思路显然更对路。
2. 核心差异一:预编译字节码 vs 运行时 JIT
这是 Hermes 和 JSC 最本质的区别,也是面试官最爱追问的“为什么”。我把它拆成几个层次来讲。
2.1 JSC 的 JIT 到底在做什么
JSC 的执行过程大致是:解析 JS 源码 -> 生成 AST -> 通过低延迟解释器(LLInt)快速解释执行 -> 收集热点函数信息 -> 触发 Baseline JIT 编译为简单机器码 -> 进一步优化(DFG、FTL JIT 或新的 B3)编译为高度优化的机器码。
这个链路听起来很美好,但代价是启动阶段的“不确定”消耗。解释器启动很快,但当热点函数被识别出来后,JIT 编译需要消耗 CPU 和内存,而且编译出的机器码和优化痕迹需要额外空间来存放。在浏览器里,页面本来就在多线程环境下运行,这些开销可以被接受;但在移动 App 里,尤其是用户点开 App 的那一瞬间,任何额外的 CPU 争抢都会直接影响首屏渲染速度。
还有一个很致命的问题:iOS 系统有一个安全机制,不允许 App 动态生成并执行可写的机器码(W^X 策略),所以 JSC 在 iOS 上无法启用 JIT,只能退化为解释执行或者预先编译好的代码。这意味着 JSC 在 iOS 上展示不出它最强的能力,反而白白背了复杂度。
2.2 Hermes 的 AOT 预编译:把“编译”这一步提前到打包时
Hermes 的思路是把工作前置。在 RN 打包构建时,有一步hermesc编译器会把 JS 文件编译成 .hbc 字节码文件,App 里加载的就是这个字节码,而不是原始 JS 源码。
运行时,Hermes 直接解释执行预先编译好的字节码,不需要再做 JS 解析、AST 生成、JIT 编译这一整套流程。你说它是“解释器”也对,因为字节码确实是逐条解释执行的;但区别在于,它消除了运行时编译的不确定性,让 App 启动时的 CPU 请求变得可预测、平稳,尤其适合弱内存、低算法的 Android 设备。
这里需要澄清一个常见误解:Hermes 不是把 JS 编译成机器码,它编译的是字节码。这和 Java 的 class 文件、Python 的 pyc 文件是一个思路。真正把它变成机器码的工作仍然在运行时完成,但 Hermes 本身没有 JIT,所以整体执行速度可能不如 JSC 的优化路径,但胜在稳定和轻量。
2.3 为什么移动端更吃这一套
桌面端用户对启动速度的容忍度相对高,但移动端用户点开 App 几秒钟没反应就会直接退出。启动阶段,App 通常同时要做网络请求、首屏渲染、本地存储初始化,如果 JS 引擎还要在这个节骨眼上做 JIT 编译,CPU 资源根本不够分。
我自己在低端安卓机上实测过,同一个业务页面,用 JSC 时冷启动耗时大约高 15%~20%,而且启动瞬间有肉眼可见的掉帧;切到 Hermes 之后,启动过程稳定很多,页面渲染也更平滑。这不是说 Hermes 比 JSC 聪明,而是它把大量计算从“用户等待时间”挪到了“开发者构建时间”,用户在启动时不再需要为运行时编译买单。
3. 内存管理、包体积与运行效率的实测差异
两个引擎的目标不同,导致它们在内存和资源占用上的表现几乎完全两个方向。这一节我会把数据层面能看到的东西讲清楚,并附上我实际测试得出的经验值。
3.1 Hermes 的移动端内存优化设计
Hermes 的设计目标之一就是“低内存占用”。它专门做了一些针对移动端的优化:
首先是删除 JIT 编译器,省下了 JIT 运行时需要的代码缓存和编译所需的临时内存。其次是采用分代式垃圾回收(Generational GC),配合“压缩”策略来减少堆碎片和对象头开销。Hermes 对字符串做了特殊处理:小字符串直接采用内联存储,避免了每次新建字符串时的堆分配开销。
官方给的数据是 Hermes 可以把 Android App 的启动峰值内存降低约 50(相对 JSC 场景),在低端机上尤为明显。我在一个中型 RN 项目上观察到,冷启动后 Hermes 的平均内存占用比 JSC 低 30%~40%。当然这个数字受业务代码影响很大,但趋势是一致的。
Hermes 还引入了一个有意思的机制叫”Concurrency“和”Lazy Compilation“,在较新版本中默认开启。简单说就是:某些函数和模块只有在真正执行到时才编译成字节码,进一步降低初始化开销。这一点在大型 RN 项目里效果显著,因为不是所有业务模块都在冷启动阶段被加载。
3.2 JSC 在移动端的实际表现
JSC 毕竟是为桌面浏览器设计的,它没有专门为移动端做极端的内存压缩和懒加载优化。iOS 上系统内置的 JSC 经过苹果多年打磨,表现尚可,但 Android 上的 JSC 需要 App 自带一个 .so 动态库,体积和内存占用都不小。
我之前测试过一个页面包含大量长列表和图片卡片的 RN 应用,在启用 JSC 时,期间内存峰值一度超过 300MB,有些低端机直接出现 OOM。切换 Hermes 后同样场景内存峰值降到 220MB 左右。当然,这里不全是引擎的功劳——Hermes 对字符串和对象头的优化确实产生了直接效果。
3.3 包体积影响:开了 Hermes 到底是变大还是变小
很多人以为 Hermes 会减小安装包,实际上它的影响要分两头看:
- JS 业务包:打包后由 JS 源码变成了 .hbc 字节码,体积通常能减小 20%~30%,因为字节码比源码更紧凑。
- 原生侧引擎:Hermes 的 .so 库体积并不小。Android 上增加了大约 1.5MB~2MB 的 native 库(具体看 ABI 数量),iOS 上增加约 2MB~3MB。
所以整体上,安装包的体积变化取决于你原来的业务代码大小。业务代码多的项目,开 Hermes 之后安装包反而会缩水;业务代码少的小 Demo,安装包可能会轻微变大。我在一个业务代码约 3MB 的项目里实测,开启 Hermes 后总安装包反而小了 800KB 左右。
| 维度 | Hermes | JSC |
|---|---|---|
| 代码执行方式 | AOT 预编译字节码 | 解释执行 + JIT 优化 |
| 启动时间 | 明显更优,尤其在低端机 | 受 JIT 预热影响,冷启动偏慢 |
| 内存占用 | 低,针对移动端优化 | 相对较高,桌面浏览器设计 |
| 包体积 | 业务包变小,引擎库稍大 | 业务包保持源码,引擎库不小 |
| iOS 适配 | JIT 不可用也无所谓 | iOS 上 JIT 被禁,优势受限 |
| 调试体验 | 需要额外代理,但整体现代 | 依赖 Metro 转发,工具链老 |
4. 开发调试与工具链差异
很多人在切换 Hermes 后遇到的第一个问题不是运行报错,而是“调试器连不上了”。这一节把调试链路、API 兼容性和常见坑讲清楚。
4.1 Hermes 的调试方案
Hermes 支持通过 Chrome DevTools Protocol(CDP)进行调试,这意味着你可以用 Chrome DevTools 来断点调试、查看 Console、观察网络请求。但注意,它不像浏览器版 JS 一样直接在 DevTools 里选目标。
正确的调试姿势是:在 App 所在的设备或模拟器上先执行adb reverse tcp:8081 tcp:8081(Android 真机必须做,iOS 模拟器不用),然后启动 Metro,在 App 设置里打开 Debug 模式,Metro 会自动启动一个 Hermes Inspector Proxy,它会和手机里的 Hermes 实例建立连接。此时打开 Chrome 访问http://localhost:8081/debugger-ui/,或者在浏览器里打开 DevTools,找到 Hermes 的调试目标,就能开始断点调试了。
React Native 0.70 以后还支持了 Hermes 的独立调试面板,在 DevTools 里可以直接看到 Hermes 的堆快照和性能数据,这比 JSC 时代友好很多。
4.2 JSC 的老调试链路
在 JSC 时代,React Native 的调试是依靠 Metro 和浏览器来做的。App 启动后,把 JS 执行环境挂到浏览器上下文中,用 Chrome DevTools 调试需要打开http://localhost:8081/debugger-ui/,执行逻辑实际跑在浏览器里,App 里的 JSC 只负责转发。这听起来有点绕,实际开发中经常会遇到断点位置偏移、React DevTools 连接不上之类的小毛病。
说实话,JSC 的调试链路相当“陈旧”,它本质上是抱着浏览器调试器的大腿,而 Hermes 的 CDP 调试更接近现代前端调试体验,这是一个很大的体验提升。
4.3 兼容性与 API 差异扫盲
因为 Hermes 是一个相对年轻的引擎,它支持的 ECMAScript 特性曾经落后于 JSC。这里我把常见的兼容性问题列出来,重要程度拉满:
- 早期 Hermes 不支持
Proxy和Reflect,在较新版本中已经支持,但使用时要留意性能损耗。 - Hermes 默认不支持完整
Intl国际化 API,需要安装intl或hermes-intl相关的 polyfill,并且在构建时启用intl支持选项。如果你的业务大量使用toLocaleString、Intl.NumberFormat,这块必须提前验证。 Symbol的基本能力支持,但某些高级使用方式(比如Symbol.iterator的复杂迭代场景)要测试。- Hermes 不支持
eval()和Function()构造器在某些安全策略下的使用,如果你的代码里有动态拼接字符串执行的逻辑,要提前改造。
React Native 官方在 0.70 之后把 iOS 也默认切换到了 Hermes,基本意味着官方认为兼容性已经足够主流业务使用。但如果你依赖比较冷门的 JS 库,建议上线前用 Hermes 模式跑一遍全量测试,尤其是涉及字符串处理、日期格式化、正则复杂匹配的库。
5. 切换与选型实操:我的经验与建议
这一节给正在做技术决策的人。Hermes 不一定在所有项目里都比 JSC 好,但绝大多数情况下,我的建议是:新项目默认开 Hermes,老项目在充分测试后尽快迁移。
5.1 什么项目该开 Hermes,什么项目该保留 JSC
如果你遇到下面这些情况,开 Hermes 基本是稳的:
- 业务方要求 Android 低端机(3GB 以内内存)也能顺畅跑起来;
- 冷启动时间是重点优化指标,需要启动提效;
- 项目业务代码量大,希望减小 JS bundle 体积;
- 团队想用现代 CDP 调试体验,不想再和旧调试链路较劲。
但如果你遇到这些情况,可能需要暂缓切换或者谨慎处理:
- 项目重度依赖
Intl,且不想引入额外 polyfill; - 依赖的某个 JS 库深度使用了 Hermes 不支持的语法或 API;
- 业务里存在大量动态生成代码、字符串化函数、
eval之类的非常规写法。
老实说,除了第三种情况,大部分兼容性问题都能用少量代码改造来解决。我见过一个项目因为几个toLocaleString的调用在 Hermes 上显示异常,最后通过引入intlpolyfill 就搞定了,耗时不到一个上午。
5.2 开启 Hermes 的具体配置步骤
Android 端的开启方式非常直接。在android/app/build.gradle里找到react配置块,把enableHermes设置为true,然后同步重新构建。React Native 0.64 以后 Android 默认就是true,0.70 以后 iOS 也是默认开启。
project.ext.react = [ enableHermes: true, hermesFlagsRelease: ["-O", "-output-source-map"], ]iOS 端的配置在ios/Podfile里,比较常见的方式是把它作为环境变量传给 pod:
ENV['RCT_NEW_ARCH_ENABLED'] = '1' # 或者直接在 Podfile 中修改 use_react_native! 参数 use_react_native!( :hermes_enabled => true )注意,iOS 切换之后需要先pod install并清理构建缓存,否则可能继续使用旧的 JSC 库。Android 切换后建议执行./gradlew clean再重新编译。每次切换引擎,都建议在 clean build 的前提下验证包体积和启动时间,否则数据没有参考价值。
5.3 切过去之后最容易踩的坑
切换到 Hermes 后会遇到几个很实际的坑,我一个个说:
- 调试器连不上。最常见的原因就是
adb reverse没执行。Android 11 及以上有些场景需要执行adb reverse --remove-all再重新绑定;iOS 模拟器则要检查 Metro 是否使用--host指定了正确地址。 - 字节码导致的安全新问题。Hermes 打包后产物是 .hbc 文件,不再是明文 JS 源码,这确实提高了逆向门槛。但注意,Hermes 有一个开源工具
hermes-dec可以把字节码反编译成可读的中间码,所以不要把字节码当成加密方案。如果业务对代码抗逆向要求高,建议额外加混淆和加固,而不是指望引擎自身。 - sourcemap 丢失导致线上报错定位困难。Hermes 打包时必须要生成 sourcemap,否则线上堆栈还原不出来。release 版配置里一定要加
-output-source-map参数,并把 sourcemap 归档到 CI 系统里。 - 某些第三方原生模块在 Hermes 上表现异常。一些旧的 RN 原生模块内部通过 JSC 的全局对象做桥接,切到 Hermes 后可能拿不到对应的全局 API,表现为运行时 Undefined。遇到这种情况,需要逐个排查原生模块的 JS 侧依赖。
6. 面试常见追问与速记要点
既然是“八股文 #005”,我来汇总一下面试官比较爱追问的问题,方便快速复习。
6.1 高频追问与参考回答
Q1:Hermes 没有 JIT,为什么反而快?因为 Hermes 把编译步骤前置到了打包阶段。运行时它加载的是字节码,不需要在用户启动 App 的时候做解析和编译。对于移动端来说,启动速度和内存占用往往比峰值执行速度更重要。Hermes 牺牲了长时间运行场景下的极限执行性能,换来了更有确定性的启动表现。
Q2:为什么 iOS 上 JSC 的优势发挥不出来?iOS 有 W^X 安全策略,不允许 App 在运行时动态生成可写可执行的机器码,所以 JSC 的 JIT 在 iOS 上是被禁用的,只能走解释执行路径。这就让 JSC 最大的优势消失,反而还要承担解释器性能和内存开销。而 Hermes 从一开始就不依赖 JIT,所以这个限制对 Hermes 没有影响。
Q3:Hermes 支持 ES6+ 全部语法吗?不支持“全部”。官方文档明确列出了支持的 ECMAScript 特性范围和限制,一些最新提案特性支持程度会落后于 JSC/V8。日常业务代码的绝大部分语法都没问题,但遇到冷门特性或者复杂正则、Intl 时,要谨慎并做验证。
Q4:Hermes 和 JSC 能共存吗?正常情况下不能。一个 RN 应用有一个 JS 执行环境,要么是 Hermes,要么是 JSC。切换是在构建层面全局控制的,不存在混用的官方方案。
Q5:Hermes 能提升所有业务的性能吗?不能。如果业务里有大量 CPU 密集型的长任务,JSC 的 JIT 优化后可能跑得更快。Hermes 的强项是启动速度和内存占用,在长任务计算场景可能不如 JSC。实际开发中这类任务一般放到原生线程,很少在 JS 线程做长时间计算。
6.2 一句话版本的速记口决
我面试前会默念这段口诀:
“JSC 是浏览器来的,JIT 是它的家底,性能上限高但启动不稳、内存重;Hermes 是 RN 自家造,AOT 预编译字节码,无 JIT 但启动快、内存省、包更小。iOS 禁 JIT,所以 Hermes 在 iOS 也不吃亏。调试走 CDP,有 Proxy/Intl 兼容性风险。”
背熟这几句话,面试官无论从哪个角度追问,你都能兜回来。
个人经验上,我强烈建议新项目直接启用 Hermes,老项目在排期允许的情况下也尽早切换。不需要过度担心兼容性,React Native 官方已经把它推成默认引擎,主流开源库基本都适配过了。真正需要认真对待的是测试覆盖和数据验证,毕竟引擎切换这件事,影响的是全 App 的 JS 执行环境,线上出问题定位成本会比较高。
