
做 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.frameworkRN 可以直接调它来执行 JS 逻辑不需要额外打包一个 JS 引擎进 App。对当时刚起步的 RN 来说这是成本最低、兼容性最好的方案。后来 Android 上通过 jsc-android 这个包来提供对应版本的 JSC 实现但 JSC 的底层设计仍然围绕浏览器场景展开。JSC 在浏览器里要跑重交互、重计算、大量 DOM 操作的页面所以它走的是“极致执行效率”路线核心手段就是 JITJust 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 路线改用 AOTAhead 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 左右。维度HermesJSC代码执行方式AOT 预编译字节码解释执行 JIT 优化启动时间明显更优尤其在低端机受 JIT 预热影响冷启动偏慢内存占用低针对移动端优化相对较高桌面浏览器设计包体积业务包变小引擎库稍大业务包保持源码引擎库不小iOS 适配JIT 不可用也无所谓iOS 上 JIT 被禁优势受限调试体验需要额外代理但整体现代依赖 Metro 转发工具链老4. 开发调试与工具链差异很多人在切换 Hermes 后遇到的第一个问题不是运行报错而是“调试器连不上了”。这一节把调试链路、API 兼容性和常见坑讲清楚。4.1 Hermes 的调试方案Hermes 支持通过 Chrome DevTools ProtocolCDP进行调试这意味着你可以用 Chrome DevTools 来断点调试、查看 Console、观察网络请求。但注意它不像浏览器版 JS 一样直接在 DevTools 里选目标。正确的调试姿势是在 App 所在的设备或模拟器上先执行adb reverse tcp:8081 tcp:8081Android 真机必须做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 默认就是true0.70 以后 iOS 也是默认开启。project.ext.react [ enableHermes: true, hermesFlagsRelease: [-O, -output-source-map], ]iOS 端的配置在ios/Podfile里比较常见的方式是把它作为环境变量传给 podENV[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 高频追问与参考回答Q1Hermes 没有 JIT为什么反而快因为 Hermes 把编译步骤前置到了打包阶段。运行时它加载的是字节码不需要在用户启动 App 的时候做解析和编译。对于移动端来说启动速度和内存占用往往比峰值执行速度更重要。Hermes 牺牲了长时间运行场景下的极限执行性能换来了更有确定性的启动表现。Q2为什么 iOS 上 JSC 的优势发挥不出来iOS 有 W^X 安全策略不允许 App 在运行时动态生成可写可执行的机器码所以 JSC 的 JIT 在 iOS 上是被禁用的只能走解释执行路径。这就让 JSC 最大的优势消失反而还要承担解释器性能和内存开销。而 Hermes 从一开始就不依赖 JIT所以这个限制对 Hermes 没有影响。Q3Hermes 支持 ES6 全部语法吗不支持“全部”。官方文档明确列出了支持的 ECMAScript 特性范围和限制一些最新提案特性支持程度会落后于 JSC/V8。日常业务代码的绝大部分语法都没问题但遇到冷门特性或者复杂正则、Intl 时要谨慎并做验证。Q4Hermes 和 JSC 能共存吗正常情况下不能。一个 RN 应用有一个 JS 执行环境要么是 Hermes要么是 JSC。切换是在构建层面全局控制的不存在混用的官方方案。Q5Hermes 能提升所有业务的性能吗不能。如果业务里有大量 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 执行环境线上出问题定位成本会比较高。