尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

React Native性能优化:Hermes与JSC引擎差异全解析

React Native性能优化:Hermes与JSC引擎差异全解析 最近在帮团队把 React Native 项目从 0.68 升到 0.72 时发现一个很多人没太在意的变化——Android 端的默认 JavaScript 引擎从 JavaScriptCoreJSC换成了 Hermes。当时我第一反应是“Meta 终于把自己那套东西扶正了”但真等项目跑起来才发现这个引擎切换带来的差异比我想象中要深得多。所以这篇就来把 Hermes 和 JSC 的来龙去脉、设计思路、实际表现以及选型时容易踩的坑一次说透。这篇内容适合谁看如果你正在维护一个 React Native 项目、准备升级版本或者面试时被问到“Hermes 和 JSC 有什么区别”但只记得“一个快一个慢”那这篇应该能帮上忙。1. 先弄清楚这两个引擎分别在什么场景下诞生1.1 JSC跟着浏览器和 App Store 一起长大的工具链JavaScriptCore 不是专门为 React Native 写的。它是 WebKit 渲染引擎的一部分也就是苹果 Safari 浏览器底下的 JavaScript 执行引擎后面也被 macOS 上的 App Store、桌面组件、Automator 这类东西拿来跑 JS 脚本。JavaScriptCore 的历史能追溯到 2002 年苹果从 KHTML/KJS 那边分叉之后一路维护到现在属于典型的“久经沙场”的老牌引擎。JSC 这套引擎的设计逻辑是围绕“浏览器里跑网页”这个场景展开的。浏览器里跑 JS 有个特点代码是动态加载的从网络下来的时候只是一堆源码文本没法事先做太多处理所以引擎必须在运行时快速地完成解析、编译、执行。这就催生了它的核心思路——JITJust-In-Time即时编译技术。简单说就是引擎在代码运行的时候一边解析一边把热点代码编译成机器码从而加速执行。JIT 的代价是它需要额外的编译时间也需要多占一些内存来缓存编译产物。在移动 App 的场景里JSC 有它的天然优势——iOS 系统里本身就内置了 JavaScriptCoreApp 可以直接调用系统版本不需要额外打包一个引擎进去。这也是早期 React Native 选择 JSC 的原因之一省了比较大的包体积成本。但这里有个隐蔽的坑iOS 上的 JSC 是系统提供的版本完全跟随系统升级开发者没法控制而 Android 系统本身没有内置 JSC所以 React Native 在 Android 上是把一整套 JSC 编进 APK 里的。结果同一个 App 在不同的平台上跑着不同版本、不同实现细节的 JS 引擎跨端一致性全靠运气。1.2 HermesFacebook 被 React Native 的性能问题逼出来的自研引擎Hermes 是 Facebook现在叫 Meta在 2019 年开源的 JavaScript 引擎开源的时候就是明确奔着一个目标去的——把 React Native 的性能尤其是 Android 端的性能捞起来。如果你是老 RN 开发者应该记得当时 Android 端的体验确实一言难尽首屏白屏时间长、内存动不动就飙高、低端安卓机器上滑动都掉帧。为什么帧率会掉一个根本原因就是 JSC 在 Android 上是运行时即时编译的。App 启动以后JS 代码从解析到编译要花时间这就像餐厅接到订单才开始洗菜切菜高峰期必然是手忙脚乱。Hermes 的解题思路很直接——把准备工作提前做完。它不是运行时编译而是支持 AOTAhead-of-Time预编译打包阶段就把 JavaScript 源码编译成字节码App 启动时引擎直接加载编译好的字节码执行省掉了运行时解析和编译的过程。所以 Hermes 不是“改一改 JSC”或者“套了个壳”而是从零设计的一套专门跑 React Native 应用的引擎。它从一开始就把目标定为移动设备——内存是受限的、CPU 是受限的、首屏加载时间是金贵的。这跟 JSC 那种“什么场景都能跑”的通用定位完全不一样。2. 设计思路的分岔路通用型 vs 专用型2.1 最根本的差异AOT 预编译 vs JIT 运行时编译这是 Hermes 和 JSC 之间最值得说的一层。JSC 走的是传统的“源码拿到手再处理”路线。在浏览器场景里这是无奈之举——你没法让所有网站都提前给你编译好代码。但在 React Native 场景里App 的 JS 代码是开发者自己打包的完全可以提前处理。Hermes 利用了这个优势。你在 Release 打包 React Native 应用时会把 JS Bundle 编译成 Hermes 字节码文件后缀通常是.hbc。这一步会经历分词、解析、AST 生成、字节码生成等完整流程。也就是说原本 App 启动时要做的“读源码、解析语法、生成字节码”这些活被打包工具提前做完了。这里有一个需要说清楚的点Hermes 虽然没有 JIT但它不等于没有优化手段。它在预编译阶段会做很多静态优化比如常量折叠、死代码消除这类编译期优化。等到运行时Hermes 依靠自身的解释器来执行字节码。解释器执行肯定没有 JIT 编译后的机器码那么快但好处是稳定没有 JIT 就意味着没有“编译暂停”带来的卡顿也不需要在运行时维护一个额外的编译缓存区。对于移动端 UI 这种交互频繁、但是单次计算量不大的场景解释器执行反而更可控。JSC 的 JIT 是一把双刃剑。它能让某些高计算量的 JS 代码跑得飞快但 JIT 的预热需要时间而且预热过程本身就是额外开销。如果你跑一段三秒就结束的短命任务JIT 可能还没完成预热代码就执行完了那这段时间属于纯亏。React Native 的业务代码恰恰就是大量这样的“短命任务”——点击一个按钮、滑动一屏列表没有长时间连续计算的热点代码可以给 JIT 去优化。2.2 内存跟启动速度的账是这样算出来的内存是 Hermes 最得意的战场。JSC 在运行时为了支撑 JIT需要在内存里保留很多中间产物解析出的 AST、编译生成的机器码、编译缓存、各类优化信息等。这些开销在 PC 和服务器上不算什么但在内存只有 2-3GB、上面还跑了一堆 App 的安卓机上就是实实在在的负担。Hermes 的内存优化从几个层面下手。第一预编译字节码本身比源码更紧凑——.hbc 文件比等效的 JS Bundle 体积要小同时解析出来的数据结构更简单不用像 JSC 那样维护庞大的源码文本映射。第二Hermes 专门针对移动设备设计了 GC垃圾回收策略它在分配策略和 GC 触发时机上做了很多调优让内存碎片化程度比传统 GC 更低。第三预编译省掉了运行时解析所需的临时内存——这部分在 JSC 上是不可忽略的尤其是大 Bundle 的情况下。这里我给你们一个很直观的类比。JSC 的做法就像你每天到公司以后才看文档开始干活工作的每一分钟都有一张摊开的图纸Hermes 的做法像你昨天就把图纸读进脑子里了今天直接动手桌面上的废纸少了很多。我还翻过 React Native 社区流传的 benchmark在 Facebook 自家的测试场景里启用 Hermes 后 Android App 的启动时间平均减少了大约一半安装包里的 JS 部分体积减少了大约 25%内存占用也有两位数的百分比下降。具体数字不同设备不同测试场景会差很多但方向是一致的Hermes 专门为移动端优化的结果是肉眼可见的。2.3 别再听人瞎传“Hermes 支持 JIT”——它压根就没有 JIT社区里关于 Hermes 是否支持 JIT 的讨论很多有些文章甚至说“Hermes 支持 JIT但默认关闭”。这个说法容易误导人。官方口径很明确Hermes 当前没有实现 JIT。它是纯解释器加预编译字节码的执行架构。为什么不做不是做不出来而是刻意不做。Meta 官方博客在发布 Hermes 时专门聊过这个问题——他们做过实验在移动设备上做 JIT 带来的性能收益会被它的内存开销和启动功耗抵消掉。与其让用户面对不可预测的性能抖动不如选择一条稳定、可控的路线。这个决策背后的思路值得借鉴性能不只是跑分更是体验的一致性。JSC 的 JIT 能跑出很高的峰值性能但这个峰值是“看运气”的——取决于代码路径、设备状态、运行时长。而 Hermes 选择了把性能下限抬高的路线每一次渲染、每一个交互都在一个稳定可控的区间里运行。3. 在 IDE 和调试器里能感受到的差异3.1 Chrome 调试和 Safari 调试的分野JSC 时代的 React Native 调试器是依赖浏览器 Developer Tools 的。JSC 在 Debug 模式下跑在 App 里然后把调试协议转发到 Chrome DevTools你在 Chrome 里就能下断点、看堆栈、跑 console。这套方案体验还行但有个老毛病——Debug 模式和 Release 模式的差异实在太大。Debug 模式下所有 JS 都在 Chrome 的 V8 引擎里执行Release 模式切回 JSC等于开发时跑的车和上线时跑的车根本不是同款。遇到诡异 bug最常见的排查路径是先在 Debug 里看一眼然后在 Release 里复现一下两边对不上就一脸懵。Hermes 在调试上解决了两件事。第一Hermes 实现了自己的调试协议可以直接在 Chrome DevTools 里连上来调试 Hermes 上真正执行的代码不用再“借”Chrome 的 V8 了。这意味着 Debug 和 Release 之间的引擎差异大大减小——虽然两个模式还是不完全一样但至少都是 Hermes 在跑而不再是“V8 和 JSC 完全两回事”。第二Hermes 的调试器能拿到字节码执行的上下文断点、单步、变量检查都做得到。对日常开发来说这已经足够用了。不过要注意Hermes 的调试能力目前还是比不上 V8 的 DevTools 那么丰富比如性能面板、内存分析的工具链没有完全打通遇到性能瓶颈还是得靠别的工具辅助。3.2 错误堆栈跟异常信息的长相完全不同这个细节是我在实际踩坑时发现的。JSC 模式下的错误堆栈就是典型的 V8/JSC 风格——一堆函数调用链还有文件名、行号、列号。但如果你开启 Hermes跑 Release 包错误堆栈会变成 Hermes 特有的一套格式而且默认情况下很多堆栈信息被压缩过——因为它跑的是字节码没有源码文本堆栈里可能只显示字节码的偏移量而不是我们熟悉的 JS 源码文件行号。这就要牵扯出一个非常重要的配置项source map。在启用 Hermes 的时候React Native 打包工具会生成 source map把字节码执行位置映射回原始源码的位置。如果你在做错误监控比如 Sentry、BugSnag一定要把 Hermes 的 source map 处理好否则线上报上来的错误堆栈会是一堆看不懂的二进制偏移量。我在团队里做过一次事故复盘上线后用户反馈某个页面报错率突然升高但 Sentry 里堆栈完全没法看当时怀疑是不是 JS 代码写错了结果发现是 Hermes 的 source map 没有正确上传。修复之后错误堆栈恢复正常定位到是某个第三方库在低版本安卓系统上的兼容问题。这个坑希望大家别踩。3.3 对 ES Next 新语法的支持进度不一样JSC 的语法支持进度总体来说是跟着 WebKit 走的Safari 能用什么JSC 就能跑什么。因为 Safari 是苹果的软件更新节奏驱动所以 JSC 对现代 ES 特性的支持相当积极——ES2021、ES2022 里的新特性往往浏览器开始支持之后JSC 就能用上了。Hermes 作为较新的引擎在语法支持上是在追进度的。它支持大部分 ES2015 的特性但有些最新的提案特性可能会滞后。比如几年前我在一个项目里用了Array.prototype.flatMap在 Hermes 上直接报错因为当时 Hermes 还没实现这个方法。解决办法通常是加 polyfill 或者 Babel 插桩转译。所以如果项目里用了很多“很新”的 JS 特性且你又必须要启用 Hermes建议在打包流程里加上 Babel 的 target 配置把preset-env的 targets 指定为hermes。Babel 有个hermestarget 选项它知道的 Hermes 支持什么、不支持什么自动帮你把不支持的特性转译成兼容写法。这个配置能省掉大量手填 polyfill 的维护成本。4. 一组真实数据启动时间、内存占用、包体积4.1 启动时间预编译的威力在这摆着这是 Hermes 最值钱的卖点也是我最初升级项目时感受最明显的地方。以我们团队一个中等体量的 RN 项目为例Android Release 包在测试机上测冷启动时间JSC 模式大概是 1.8 秒左右切到 Hermes 之后降到 1.2 秒左右。这里说的冷启动是从点击 App 图标到首屏画面完全渲染出来的时间不是纯 JS 引擎启动时间——后面还包括了图片加载、网络请求、首屏布局等环节。但 JS 引擎部分确实从大约 500 毫秒压缩到了 200 毫秒左右。为什么会省下这么多时间因为 Hermes 不需要在启动时解析源码。解析 JavaScript 源码是个相当重的过程——字符串拆成 token、token 构造成 AST、AST 生成字节码每一层都是 CPU 密集型操作。JSC 做这些事还要受设备 CPU 性能的限制低端机上的差距会更明显。Hermes 直接加载 .hbc 字节码文件。字节码文件本身就是二进制的结构紧凑加载后可以直接执行省掉了源码解析这一大块时间。对于首屏对体感要求极高的场景这一步优化非常值钱。4.2 内存占用为什么低内存安卓机上 Hermes 更香内存占用是 Hermes 的主场但我得说清楚一点不要指望 Hermes 能让你的 App 内存占用瞬间砍半。引擎本身只是 App 内存占用的一部分真正的内存大头通常是你自己的业务数据、图片缓存、网络请求等。但引擎部分是可以省出来的。按照我们当时在 Android 8.0 的测试机上用dumpsys meminfo的观察切到 Hermes 之后Java / Native 堆里的额外开销减少了大约 60-80MB。这个数字对高配手机来说可能不痛不痒但对只有 2GB 内存的机器来说直接关系到进程会不会被系统杀死。有一个点值得拿出来细说JSC 的 JIT 编译缓存是常驻内存的。热点代码一旦被编译成机器码就会一直占着内存不放。这些机器码可能有一二十 MB 甚至更多。Hermes 没有 JIT所以不存在这块开销。同时 Hermes 的 GC 对短生命周期对象的回收策略更激进频繁创建临时对象的场景比如列表渲染它的内存曲线更平稳。4.3 包体积别只盯着引擎本身包体积这块要分两层看。第一层是引擎库本身的体积。JSC 在 Android 上编进 APK 大概是 30-40MB按架构分arm64 和 armeabi-v7a 各算一份的 so 库。Hermes 的 so 库跟 JSC 在同一个量级但略小而且 Hermes 还附带一个编译器工具链不过那个只在打包阶段用到不会编进 APK。第二层是 JS Bundle 的体积。因为 Hermes 用的是 .hbc 字节码它比源码文本更紧凑。我们项目当时的 JS Bundle 从源码 4.2MB 变成了 .hbc 的 3.1MB大约省了 25%。别小看这 1MB 差距在 Android 的 APK 下载体积、内存 map 加载上都是有感的。但要注意这个体积优势会被一个东西吃掉——多架构配置。如果你没有做 ABI 拆分Hermes 和 JSC 一样会为每种 Android CPU 架构分别编一个 .hbc最终 APK 变肥。实际优化时建议开启enableSeparateBuildPerCPUArchitecture配置把各个架构分开打包然后上架 Play Store 的 AAB 格式让用户只下载适配自己设备的版本。5. 怎么选引擎全看你的业务形态5.1 React Native 新项目直接无脑选 Hermes 没错如果你是从零开始一个新的 React Native 项目现在是 2024 年直接选 Hermes 几乎不用犹豫。从 0.70 版本开始Hermes 就是 React Native 的默认引擎。Meta 后续的优化、新特性、性能改进全部优先适配 Hermes 而不是 JSC。社区生态也逐渐倒向 Hermes——不少第三方性能监控工具、崩溃收集 SDK、动画性能库默认就是为 Hermes 调优的。在选型上我给一个很实际的观点新项目跟着官方默认走通常是最省力的决策。因为默认引擎意味着社区踩过坑最多、教程最多、兼容性验证最充分。你不需要为了“性能跑分比别人高”而去选一个冷门的组合那是在给自己找麻烦。5.2 WebView 场景还是得靠 JSC 那套生态如果你的 App 大量依赖 WebView 渲染网页同时还要在原生层跑 JS 跟网页交互那情况就不一样了。JSC 的优势在于它是 WebKit 生态的一部分和 WebView 的配合是天然的。iOS 上系统自带的 JavaScriptCore 可以直接跑Android 上有基于 JSC 封装的接口。如果你需要原生层执行 JS 库、跟 WebView 里的页面共享某些 JS 全局上下文JSC 的兼容性会更好——毕竟它本来就是在浏览器环境下练出来的。Hermes 虽然也能在原生 App 里嵌入执行 JS但它是为了 React Native 优化的跟 WebView 的互动、浏览器 API 的模拟能力反而不如 JSC。我见过有人尝试在 WebView 场景里用 Hermes 作为 JS 执行后端结果遇到一堆 DOM/BOM API 缺失的问题最后只能放弃。5.3 计算密集型的 JS 任务两个都不够用有个残酷的事实如果你的业务里有大量 CPU 密集型的 JS 计算那不管是 Hermes 还是 JSC 都不理想。JS 本身是单线程的再快的引擎也逃不过这个限制。Hermes 因为纯解释执行面对高计算量的代码峰值性能可能比 JSC 的 JIT 跑满之后还要差一些。JSC 虽然能在长时间运行的高计算量场景里展示 JIT 的威力但那需要代码循环足够“热”才能触发优化而且预热期间的表现也不稳定。真正适合的方案是把重计算放到原生线程比如通过 JSI 调用 C 或者平台原生代码或者用InteractionManager把大任务拆分成小任务避免阻塞 UI 线程。如果你在面试中被问到“Hermes 还是 JSC 性能好”一个比较完整的回答是动量的——启动性能和内存占用上 Hermes 优势明显高负载计算峰值性能 JSC 理论上更强但因为 RN 业务基本是 IO 密集和 UI 交互为主Hermes 的稳定性更重要。6. Hermes 的 hbc 字节码性能与反编译的现实博弈6.1 把 React Native 代码变成 .hbc 之后发生了什么Hermes 的 AOT 编译过程在官方工具链里叫hermesc。在 React Native 打包 Android Release 包时构建流程会自动调用hermesc把 JS Bundle 编译成 .hbc 文件。这个过程有几个好处除了前面说的体积变小、启动加快还有一个附加效果——不再直接暴露源码文本。在 JSC 模式下打包后的 APK 里放的是 JavaScript 源码文本。把 APK 解压出来用任意一个文本编辑器打开 index.android.bundle你的 JS 代码就一览无余了。很多公司的业务代码就这么裸奔在 APK 里谁下载谁就能看。换到 Hermes 之后APK 里放的是 .hbc 字节码普通解压看不到源码看起来安全了不少。但这里必须泼一盆冷水.hbc 并不是真正的代码加密它只是编译产物不是密文。6.2 反编译工具能还原到什么程度社区里关于 Hermes 字节码反编译的讨论一直没断过。这跟 V8 的字节码类似理论上是可以被逆向的。目前开源社区已经有若干工具能解析 .hbc 的格式提取出部分字符串、函数名、甚至近似还原出控制流结构。有一个常见误区需要澄清反编译不能恢复出“一模一样的原代码”。它还原出的更像是一个“逻辑等价但面目全非”的东西——变量名可能会变成x1、x2这种注释全部丢失函数名有些还能看到有些会被混淆掉。对攻击者来说读懂这段还原代码的成本可能比直接读源码高得多但也绝对算不上“无法破解”。我做过一个实验把一个简单的 RN demo 打包成 .hbc然后用第三方工具解析看看到底能还原出什么程度。结果是字符串常量一目了然——比如接口 URL、AppKey、密钥这些写在代码里的明文几乎原样躺在那里。函数结构也能还原出大概但逻辑细节需要花时间去理。6.3 对代码安全的一点现实建议所以如果你问“Hermes 字节码能不能保护我的代码”我的答案是它能提升门槛但不能提供绝对安全。真正敏感的东西不要放进 JS 代码里。比如后端 API 的密钥、加密密钥、第三方服务的 secret——这些无论 JSC 还是 Hermes 都保护不了因为运行在客户端的代码终究有办法被逆向。客户端代码的安全基线应当是密钥放服务端客户端只放临时凭证并做好过期机制敏感算法下沉到原生 C 层做混淆打包。另外如果你担心业务核心逻辑被逆向可以考虑在 Hermes 基础上再叠加一层混淆——在 JS 层用javascript-obfuscator先混淆再交给 hermesc 编译。虽然性能上会有一点损耗但对提升逆向难度有明显帮助。安全从来是成本跟收益的博弈没有一劳永逸的答案。最后再说几句实操层面的体会从我这两年切换 Hermes 的实践看最大的收获不是启动速度快了多少而是线上运行状态的确定性变高了。JSC 时代时不时冒出来一个只在 Release 模式出现的诡异 bug查半天最后发现是 Debug 和 Release 引擎不一致导致的。换到 Hermes 之后这类问题基本绝迹了。如果你正准备升级到 Hermes有几点实操建议收尾时给你升级后一定要在尝鲜用户里灰度观察一两个版本重点关注崩溃率、页面加载耗时、低端机内存残留这三个指标同时把 source map 上传到你的错误监控平台确保线上堆栈可读最后检查一下代码里用了哪些较新的 ES 特性必要时加 Babel 的 Hermes target 配置兜底。引擎这东西用户基本感知不到但背后影响的是 App 的冷启动体感、低端机存活率、开发排查效率。把这层基础打扎实了上层业务才能跑得稳。
返回列表