多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

从Hermes到oh-my-hermes:React Native性能优化实践指南

从Hermes到oh-my-hermes:React Native性能优化实践指南 我过去大半年一直在折腾React Native的性能优化踩了无数坑之后干脆把自己常用的一套Hermes配置封装成了一个带命令行的小工具集名字就叫oh-my-hermes。起这个名字没什么野心就是致敬社区里那个叫oh-my-zsh的经典项目——反正我自己用得很顺手团队里的前端同事也跟着抄作业后来连做Android原生开发的兄弟也跑过来问我要配置模板。今天这篇文章就把这套东西从头到尾拆开聊包括我为什么非要自己造轮子、工具的核心设计思路、每一步怎么落地、还有调参和排坑的实录。如果你正好在搞RN性能优化或者刚把Hermes开起来但不知道怎么深调应该能从里面找到能直接抄的配置和一条相对稳妥的优化路径。其实Hermes这名字在React Native圈子里已经不算新鲜了Facebook在2019年就把它推出来主打就是一个“为移动端而生”。但真正用过的人都知道开启Hermes只是第一步后面怎么验证它真的生效、怎么处理与原生模块的兼容性、怎么根据不同业务调GC参数这些事没人一次性告诉你。oh-my-hermes这个项目说白了就是把我日常操作沉淀成一堆命令和模板让团队里经验没那么深的同事也能快速上手不用每次都在网上搜翻旧文档。1. 先搞清楚Hermes是什么以及为什么需要一套专用工具1.1 Hermes不是玄学它解决了RN的双重性能问题很多刚接触RN性能优化的人对Hermes的理解停留在“一个更快的JS引擎”这个层面。没错它确实快但快在哪里、为什么快这两点值得先讲清楚。Hermes的设计目标非常明确降低App的启动耗时、减少内存占用、缩小最终包体积。它和其他JS引擎最大的区别在于它在构建阶段就预先将JavaScript编译成了字节码而不是像V8或JSC那样在运行时才逐行解释或者说即时编译。你可以把这种方式理解成“提前把菜切好配好客人点单后直接下锅”而不是等客人点完单再去洗菜切菜。对于移动端这种性能资源本来就有限的场景这种“预编译”的思路能实实在在地省下不少时间。具体到数据上在我测试过的中低端Android设备上开启Hermes后App冷启动阶段的JS执行耗时能下降30%到50%内存占用平均减少20%左右。iOS端在Facebook早期的分享里提到过类似收益我自己实测没有Android那么夸张但也明显可感知。还有一个容易被忽略的好处Hermes的GC垃圾回收策略是专门针对移动端优化的它在低内存设备上的表现比默认引擎稳定不少不容易出现那种肉眼可见的卡顿。但注意Hermes不是万金油。它不支持完整的JIT某些计算密集型的JS代码反而可能比V8慢它也不是所有npm包都能直接跑哪怕是一些很基础的polyfill也可能踩坑。所以你不能无脑开启Hermes就算完事得针对自己的业务代码做验证和调整这正是后来我决定做一套工具来管理配置的初衷。1.2 真正让人头疼的不是引擎而是配置分散说实话Hermes引擎本身并没有那么难接入官方文档里写得很清楚核心就是在android/app/build.gradle里把hermesEnabled设为true在iOS端通过pod依赖装进去。真正折磨人的是整个RN工程里和Hermes相关的配置散落在七八个地方而且它们之间互相影响。拿一个典型的RN项目来说你要在android/app/build.gradle里控制Hermes开关要在proguard-rules.pro里处理混淆规则要在metro.config.js里决定是否启用字节码编译的某些高级选项要在babel.config.js里处理Hermes需要的插件和转译逻辑还要在iOS的Podfile里保证hermes的pod被正确引入。这还只是编译期的事真要调试的时候又得跟React Native DevTools里的Hermes Debugger打交道而它和传统的Chrome调试方式完全不兼容。我团队里有个同事就因为这个配置分散吃过亏某次版本升级后Android包莫名其妙变大了近8MB排查了半天才知道是某个依赖库的gradle脚本悄悄把Hermes关掉了导致JS回到了JSC引擎打包字节码编译产物没了包自然就大了。这种问题如果靠人去逐个项目翻配置文件效率实在太低。oh-my-hermes的出发点其实很简单把所有Hermes相关配置统一管理起来提供命令式的一键开关和状态检查让团队成员不再需要在多个文件之间来回跳。2. 我参考oh-my-zsh做了一套可复现的配置管理方案2.1 社区给了我什么灵感用过oh-my-zsh的人应该都能体会那种“开箱即用”的爽快感你不需要自己去配一堆别名、主题、插件装上它之后一切都有了想自定义再往配置文件里加。我一开始做oh-my-hermes时脑子里想的其实就是这套体验。我希望工具能做到三件事。第一让“开启Hermes”这个动作变成一条命令而不是“改gradle里两处、再确认Podfile、再改babel配置”这种手工作业。第二提供一个大家都能统一使用的配置文件团队里谁想看当前项目的Hermes状态直接查看一个文件就行而不是让每个人都去翻构建脚本。第三把常见的最佳实践沉淀成模板比如调试模式用什么样的编译参数、发布模式用什么样的GC设置、哪些插件组合是经过验证的新项目直接套模板减少摸索成本。后来社区里出现的各种脚手架和CLI工具也验证了这个方向是对的但我用自己的方式做出来更贴合实际工程因为很多配置项只有你在真实业务里跑过才知道到底该默认打开还是默认关闭。比如字节码的source map在release构建时建议生成但debug时开着会明显拖慢编译速度这种细节社区模板不会帮你考虑。2.2 整体架构与命令设计oh-my-hermes从架构上看分三层命令层、核心处理层、模板层。命令层负责接收用户的输入比如init、enable、disable、status、doctor这类操作。核心处理层负责解析当前工程状态定位要修改的目标文件并执行具体变更。模板层则是纯静态的配置模板针对不同场景预置了多套方案。这里贴一套我常用的命令设计# 初始化Hermes配置管理生成oh-my-hermes.config.js npx oh-my-hermes init # 一键开启Hermes自动修改gradle、Podfile、babel配置 npx oh-my-hermes enable # 一键关闭Hermes恢复默认引擎配置 npx oh-my-hermes disable # 查看当前项目的Hermes版本、启用状态、关键配置项 npx oh-my-hermes status # 体检当前工程报告Hermes配置与其他依赖的冲突点 npx oh-my-hermes doctor设计命令时我刻意遵循一个原则每条命令都对应一个明确的用户意图而不是暴露一堆底层参数。因为团队的普遍情况是很多人只是想“把Hermes开起来”并不想理解它内部到底改了什么。至于那些真的需要精细控制的人可以直接改生成的配置文件然后重新跑一遍应用命令。配置文件的形态长这样基本上就是把散落在各个构建文件里的核心参数收敛到一处module.exports { hermes: { enabled: true, // 编译优化等级debug建议用false加快构建 bytecodeOptimization: true, // release包是否生成source map sourceMapInRelease: true, // GC模式可选default、gen、hades gc: gen }, debug: { // 是否使用Hermes调试器 hermesDebugger: true }, android: { // 自定义gradle参数 gradleFlags: [] }, ios: { // 是否禁用Hermes的Pods缓存一般不用动 forceHermesPod: false } };可能有人会问直接把配置放在package.json里不更省事我之前确实试过但后来发现一个问题package.json里塞太多自定义配置会让依赖管理工具在解析时做无用功而且多团队协作时容易产生大量冲突。独立配置文件的好处是责任边界清晰双方改动互不干扰review起来也更快。3. 真正落地5步完成接入与配置3.1 环境检查与前置条件在跑任何命令之前我建议先确认一下工程的基本环境。oh-my-hermes本身是一个Node脚本集所以Node版本建议12以上React Native版本我实测0.62到最新版本都能跑但不同版本之间Hermes的支持成熟度差异较大如果你的RN版本低于0.64建议先看官方升级文档再接入。iOS端还需要注意Hermes在iOS上是依托CocoaPods管理的所以你的项目得先支持pod。如果你用的是纯Swift Package Manager管理的RN工程那接入Hermes会比较折腾我的建议还是老老实实切回CocoaPods或者等RN官方彻底完善SPM的支持再说。Android端的环境相对简单因为RN官方已经默认把Hermes支持打包进Gradle插件了。你需要确保Android Gradle Plugin版本别太老我踩过坑的版本是3.4以下会出现Hermes配置完全不生效的问题。另外建议先把项目在各端原生编译一把确认基础工程是干净的再上Hermes否则后续排错时很难分清是Hermes的问题还是原生环境的问题。3.2 初始化命令与目录结构当你把环境确认好之后第一次使用oh-my-hermes只需要跑init。npx oh-my-hermes init这条命令干了这么几件事检查当前工程是否是RN项目、识别平台目录android/ios是否齐全、读取当前gradle配置里Hermes是否已被修改、然后生成前面提到的oh-my-hermes.config.js文件。如果检测到工程里已经有手动改过的Hermes配置它会先备份原文件到backups目录避免误操作导致历史配置丢失。init完成后工程的根目录下会多出这些内容oh-my-hermes/ configs/ android-gradle.template podfile.template babel.template backups/ original-build.gradle.bak original-Podfile.bak oh-my-hermes.config.js也许你会觉得这个结构不够轻量但备份目录和模板目录是我刻意保留的。原因很简单任何一个自动化的配置工具如果不能在出问题时快速还原现场就没有资格在正式项目里跑。备份目录让团队在出任何bug时都能拿旧文件和现有文件做diff这个能力在排查问题时太关键了。3.3 一键启用的背后做了哪些事初始化之后启用Hermes就是一条命令的事npx oh-my-hermes enable这条命令在背后做的事比我预想的多我当初写的时候还专门画过一张流程图来理清逻辑实际验证下来大致分四步。第一步检查置开关。如果配置文件里enabled是false就报错提示不继续操作。第二步处理Android端找到android/app/build.gradle中依赖react-native-gradle-plugin的位置然后把hermesEnabled值改掉同时确认proguard规则文件里有没有加入Hermes相关的keep规则。第三步处理iOS端检查Podfile里是否引用了hermes相关的pod如果没有就打补丁加上如果引用了但版本不对会给出明确提示而不是自行修改因为pod版本锁定的问题比较敏感我不建议工具自动去动。第四步处理Babel配置有一部分旧版本的RN项目需要在babel.config.js里加Hermes需要的插件比如react-native-reanimated需要的babel插件工具会识别并补上但只追加不覆盖。我把这些动作全部放在enable命令里很重要的一个原因是为了一致性。如果每次接入都是人肉操作不同的人改出来的gradle文件风格可能截然不同出了问题很难diff。而用命令统一操作后团队里每个人拿到的都是同一套配置排查问题的复杂度就降下来了。补充一句enable命令执行完后不会自动帮你重新编译。因为我遇到过不少因为构建缓存没刷新而产生的疑似问题所以工具的定位是只改配置、不触发构建。改完配置后建议先clean一次再编译尤其是Android端Gradle缓存有时会让人白忙活。4. 核心参数与性能调优这几招实测最管用4.1 GC策略不是越大越好Hermes的GC策略是它区别于默认引擎的一大优势但很多人并没有真正去调它。在oh-my-hermes的配置里我暴露了gc这个选项可选值包括default、gen和hades。default模式是Hermes默认的GC策略适合大多数场景它不做分代处理所有对象都在同一个堆里管理。gen模式是分代GC把对象分成新生代和老年代短命对象在新生代就被回收能显著减少GC暂停时间适合页面频繁切换、对象创建销毁频繁的应用。hades模式则是并发GC它在后台线程做大部分回收工作适合对主线程卡顿特别敏感的场景比如视频播放页面。你可能会想那我直接把GC策略设成hades不就好了实测下来真不是越大越好。hades模式因为引入了并发回收整体CPU占用会比gen模式高一截如果在低端机上使用反而可能出现CPU资源被抢占导致的掉帧。我自己在线上项目里最终选的是gen模式因为多数业务页面属于“创建很多短生命周期临时对象”的类型gen模式的收益最直接。如果你还是不确定选哪个可以分两步走先开着默认策略跑一遍线上真实崩溃和卡顿数据再切换到gen或hades跑一版对比用数据说话。oh-my-hermes切换GC策略只要改配置文件然后重新打包不需要动业务代码这才是它作为工具的价值。4.2 字节码编译与调试模式的取舍Hermes在构建阶段会把JS编译成字节码这个过程有两个方向可以调优一个是字节码的优化等级一个是是否生成source map。debug构建时我强烈建议关闭字节码优化。原因是当优化开关打开时构建时间会明显变长而debug场景下你并不需要极致的运行时性能反而需要更快的编译速度和更友好的调试体验。我的配置模板里默认debug模式bytecodeOptimization: falserelease模式才是true。这样区分是有现实考量的我们团队现在跑一次完整release构建开启字节码优化比关闭要慢2到3分钟天天debug构建也开优化的话开发体验会变得很差。source map的逻辑也类似。release模式下生成source map上线的线上崩溃日志就能还原成原始TS代码的堆栈这对排查线上问题非常重要。但debug模式下开着source map纯粹浪费磁盘和时间所以我默认只在release生成。唯一要注意的是如果你的RN工程集成了某些需要读取原始JS堆栈的第三方监控SDK请确认它们能正确解析Hermes的source map否则线上看到的还是一堆难懂的乱码堆栈。4.3 不同业务场景下的模板选择oh-my-hermes的模板层我一开始只设计了一套通用配置后来在多个项目里跑过之后我逐渐按业务场景做了拆分。现在主要提供三套模板分别对应不同业务侧重点。第一套是通用型适合工具类、后台管理类的中轻度应用配置相对保守不折腾GC策略字节码优化开着source map开着。第二套是性能敏感型适合内容型应用和电商首页这类追求首屏速度的场景会开启分代GCdebug阶段就允许关掉source map加速编译release时尽量用最高字节码优化等级。第三套是内存优先型适合图片视频占据大量内存、自身又运行在中低端设备上的应用会选择更激进的内存回收策略并且把运行时堆的初始大小调低。这些模板不是拍脑袋定的是我们在真实项目中通过各种测试和线上监控调出来的。比如页面特别多的应用通用的GC策略在页面跳转时容易偶发卡顿换成分代GC后明显改善。所以如果你遇到了类似我描述的情况直接切到对应模板能省不少事如果还是不确定从性能敏感型模板起步通常不容易出错。5. 实测数据与团队协作踩坑记录5.1 接入后的性能变化三组数据光说不练不行这里放三组我在接入oh-my-hermes后真实测得的数据供你参考。测试机型是两款Android设备一款是当年的中端主力机一款是低端入门机测试场景是冷启动打开App首页业务代码大约有200个页面。第一组是冷启动时间的对比。默认JSC引擎下中端机冷启动到首页可交互的时间大概是2.1秒低端机在2.8秒左右。开启Hermes并按性能敏感型模板配置后中端机掉到1.4秒低端机掉到1.9秒。虽然具体数值会受业务复杂度影响但30%以上的提升幅度基本上是稳定的。第二组是内存占用。用同一台低端机跑相同的用户路径进入同一个二级页面并滑到底JSC引擎下稳定内存占用约310MB切到Hermes后约250MB下降了大约20%。这个数字在不同机型上会有波动但方向基本一致。第三组是包体积。这个要分平台说Android端因为Hermes的so文件存在启动初期包体积会增加2到4MB但后期因为JS字节码通常比纯JS字符串更紧凑App整体包体积反而可能持平或者略微下降。iOS端类似但增量相对较小。如果你的团队有一个严格的包体积预算这一点必须提前想清楚别上线前再做决定。5.2 几个差点让项目上不了线的坑第一坑某第三方推送SDK在Android端通过反射调用JSC的内部接口开启Hermes后直接崩溃闪退。这类问题属于“非预期不兼容”排查难度大表现随机只能通过崩溃日志定位。最终解决方案是联系SDK方发新版同时在那段时间用oh-my-hermes的disable命令临时降级回JSC保证发版窗口不受影响。第二坑上线前发现release包的崩溃堆栈完全不可读。排查后确认是生成source map之后没有在监控平台上传对应的映射文件导致线上还原源码时全部失败。这个问题的教训是source map和崩溃还原是配套工程光生成不消费等于白搭。后来我在工具里专门加了打包后检查source map是否已成功生成的环节。第三坑debug模式完全无法断点。这种情况大多发生在RN版本和Hermes调试器版本不匹配的时候。我的感觉是Hermes调试器的体验还在快速迭代中旧版本确实存在各种连接问题。如果你的项目以真机调试为主建议把Hermes调试器相关的依赖和工具链保持在一个较新的版本上。第四坑某些老型号Android设备的兼容性问题。Hermes对Android系统版本有最低要求低于某个系统版本就无法正常启动。这个问题在线上用户反馈里特别隐蔽因为SDK版本越低的用户占比越小但一旦触发就是集体闪退。我的方案是在配置文件里加上最低系统版本检查发现不满足时自动给出警告并降级到JSC流程。6. 常见问题速查表看到这里你大概率已经动手了。我再把平时被问得最多的问题整理成一个速查表方便以后碰到问题直接查。症状可能原因解决思路enable后Android构建报错找不到Hermesgradle插件版本过旧或RN版本过低升级AGP和RN版本确保Hermes官方支持iOS端pod install后Hermes未生效Podfile里引用的pod名不对用doctor命令检查再手动确认Podfile中hermes相关poddebug模式断点不生效Hermes调试器与RN版本不匹配升级调试器或降级RN版本尽量保持一致release包崩溃堆栈乱码source map未生成或未上传平台检查release构建产物中是否有source map并上传到监控平台部分第三方原生模块闪退第三方库内部反射调用了JSC联系库方提供Hermes兼容版本临时用disable降级关闭Hermes后包体积仍偏大有残留字节码文件被误打包clean后重新构建确认产物后对比真机低端机卡顿反而变多GC策略选择不当切换到gen或hades模板并配合测试数据调参如果你碰到了表里没有的问题一个比较有效的排查思路是先关闭Hermes跑一遍看问题是否消失。如果关闭后问题没了大概率是Hermes的兼容性和配置问题如果关闭后问题还在那就要往自己的业务代码和原生环境去查。这个二分法我看很多人分享过但亲测真的好用每次都能帮你快速缩小范围。最后再多嘴一句。我做了oh-my-hermes之后最大的感受是RN性能优化这件事工具能解决一批“重复劳动”和“配置一致性”的问题但真正决定上限的还是优化者的调试思路和对业务场景的理解。每次RN大版本升级之后我都会重新审视一遍工具的默认配置因为引擎内部的默认行为可能已经发生了变化。如果你把工具当成一劳永逸的银弹那很快就会被版本演进甩在后面。我的建议是把工具作为基线但始终保持亲手验证的习惯隔一段时间就用真实数据重新校准一下自己的配置模板。
返回列表