储贻锋,携程无线平台研发部基础框架组资深Android研发,目前主要负责CRN Android端和携程Android基础架构的维护与开发工作。
Facebook在Chainreact2019大会上正式推出了新一代JavaScript执行引擎Hermes。Hermes是个轻量级的JS引擎,专门对Android上运行ReactNative进行了优化。我们第一时间在 CRN 项目中集成了Hermes, 并做了深度调研。
自ReactNative推出以来,有大量的APP接入并使用,其中也包括大型应用的主流程业务。随着业务复杂度不断上升,性能问题变得无法忽视。
在分析性能数据时,Facebook团队发现 JavaScript 引擎是影响启动性能和应用包体积的重要因素。由于JavaScriptCore最初是为桌面浏览器端设计,相较于桌面端,移动端能力有太多的限制,为了能从底层对移动端进行性能优化,Facebook团队选择自建JavaScrip引擎,设计了Hermes,限于iOS AppStore审核限制,目前仅用于Android平台。
Chain React大会上官方给出了Hermes引擎一组数据:
从页面启动到用户可操作的时间长短(Time To Interact:TTI),从4.3s减少到2.01s
App的下载大小,从41MB减少到22MB
内存占用,从185MB减少到136MB
CRN先前做过框架代码拆分和预加载、业务代码懒加载、业务代码预加载等性能优化方案,正困惑于如何更近一步进行性能优化。当看到Hermes这三个关键指标都有了显著的提高,非常激动,觉得Hermes是非常好的一个方向,接下来我们就来了解Hermes的使用和实测性能数据。
Faceback团队已经将Hermes工具上传到了npm : hermesvm 。hemres工具可以直接运行JS代码、转换字节码并且提供非常多的参数进行调优控制。
这里介绍一下hermesvm执行JS代码和转换bytecode功能。
// 创建hermes_test文件,内容:print("This is Hermes Demo");
vim hermes_test.js
// 直接执行纯文本js
~/node_modules/hermesvm/osx-bin/hermes hermes_test.js
This is Hermes Demo
// 转换成bytecode
~/node_modules/hermesvm/osx-bin/hermes --emit-binary hermes_test.js -out hermes_test.hbc
// 执行字节码
~/node_modules/hermesvm/osx-bin/hermes hermes_test.hbc
This is Hermes Demo
主流JavaScript引擎,例如JSC、V8、SpiderMonkey等几乎都是为了桌面端浏览器服务的,Hermes针对移动终端设备的特点做了一些优化,其中最重要的我们认为是以下两点:
现代主流的JavaScript引擎在执行一段js代码的大概流程是:
先读取源码文件
解析源代码并转换成字节码(bytecode)
最后执行
在运行时解析源码转换字节码是一种时间浪费,所以Hermes选择预编译的方式在编译期间生成字节码。这样做一方面避免了不必要的转换时间,另一方面多出的时间可以用来优化字节码,从而提高执行效率。
为了加快执行效率,现在主流的JavaScript引擎都会使用一个JIT编译器在运行时通过转换成机器码的方式优化JS代码。Faceback团队认为JIT编译器有主要俩个问题:
要在启动时候预热,对启动时间有影响;
会增加引擎size大小和运行时内存消耗;
基于这俩点对性能指标的影响,Faceback团队决定不实现JIT编译器。
这里所谓放弃JIT,有两点需要再解释一下:
纯文本JS代码执行效率降低。放弃JIT,是指放弃运行时Hermes引擎对纯文本JS代码的编译优化。我们的验证数据也表面,纯文本的JS代码执行,Hermes引擎明显比JavaScriptCore慢。
对RN代码的动态性无影响。由于Hermes仍然可以执行纯文本的JS代码,并且可以支持动态读取bytecode, 因此对RN的动态性并无影响。
1. 升级最新react-native-cli
npm install -g react-native-cli
2.初始化最新react-native工程,最新版为0.60.3
react-native init HermesDemo
3. 开启hermes, 编辑HermesDemo工程 android/app/build.gradl文件
project.ext.react = [
entryFile: "index.js",
- enableHermes: false // clean and rebuild if changing
+ enableHermes: true // clean and rebuild if changing
]
4. 使用Relase包体验Hermes带来的速度提升
react-native run-android --variant release
git clone https://github.com/facebook/react-native.git // 需要切换到Hermes release节点,比如:eec4dc6
cd react-native
npm install
./gradlew :RNTester:android:app:installHermesRelease // 使用生产环境hermes
分析react-native react.gradle源码可以看到,如果打开了Hermes开关,会在原先打包RN代码的bundleXXXJsAndAsset task后面追加执行一段Hermes转换命令: hermes --emit-binary -out xxx。
...
// 1. 执行标准RN打包
commandLine(*nodeExecutableAndArgs, cliPath, bundleCommand, "--platform", "android", "--dev", "${devEnabled}",
"--reset-cache", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir,
"--sourcemap-output", jsPackagerSourceMapFile, *extraArgs)
...
...
// 2. 将打包好的jsbundle文件转换成字节码
if (enableHermes) {
commandLine(getHermesCommand(), "-emit-binary", "-out", jsBundleFile, jsBundleFile, *hermesFlags)
}
...
为了进一步抽象JavaScript执行层,RN底层创建了JSExecutor和Runtime接口,并把大部分业务逻辑放到了实现了JSExecutor的JSIExcutor.cpp中。对于JavaScript执行引擎来说只需要实现Runtime接口即可对接RN框架。
JavaScriptCore的Runtime实现类是JSCRuntime。相应的,此次Hermes升级,底层创建了HermesRuntime。
// JSCRuntime.cpp jsc Runtime
class JSCRuntime : public jsi::Runtime
// hermes.h hermes Runtime
class HermesRuntime : public jsi::Runtime...
每一种JSExecutor都提供了创建类XXXExecutorFactory来创建相应实例,并且提供了相应的Java对象。
RN框架在初始化ReactInstanceManager的时候需要传入JavaScriptExecutorFactory。如果要切换JavaScript执行引擎只需要在ReactInstanceManager创建的时候做控制即可。
官方的控制流程是,优先加载jscexecutorso,如果成功则使用JSCRuntime,否则使用HermesRuntime。
private JavaScriptExecutorFactory getDefaultJSExecutorFactory(String appName, String deviceName) {
try {
// If JSC is included, use it as normal
SoLoader.loadLibrary("jscexecutor");
return new JSCExecutorFactory(appName, deviceName);
} catch(UnsatisfiedLinkError jscE) {
// Otherwise use Hermes
return new HermesExecutorFactory();
}
}
由此可见无论是对于RN JS代码的打包还是Native代码逻辑的更改,升级Hermes的成本都非常低。
通过上面的Hermes集成分析可知,Hermes对整个RN原有架构的侵入是极少的,甚至做到了可插拔式接入。我们很快将Hermes集成到携程CRN框架,并和原先的JavaScriptCore引擎以及社区提供的V8引擎做了比较。
经过我们的数据验证,Faceback团队提出的关键性指标相较于原先的JSC都有了显著提高。
首屏渲染速度:bytecode代码执行情况下,Hermes比JavaScriptCore要快。在携程App中,拿门票业务做了验证,在做了预加载的情况下,首屏加载速度依然可以提升约15%。而V8的表现就非常糟糕了。
Native so size:RN所依赖的必要so库,Hermes比JavaScriptCore减少了约16%(单armeabi架构压缩后降低了0.5M左右),V8则要远大于Hermes和JavaScriptCore。
内存:拿 RNTester 工程测试进入RN页面滑动进入若干页面并退出之后,内存的波动情况比较可以看到,V8和Hermes内存增长要更加平滑。
CPU:拿 RNTester 工程测试进入RN页面滑动进入若干页面并退出之后,对比CPU波动情况。Hermes明显好于V8和JavaScriptCore。
另外通过我们的测试,Hermes在执行字节码和文本JS上有一些很有意思的特性,这些特性让升级成本变得非常低:
Hermes支持执行纯文本的js
支持动态加载纯文本js或者bytecode
支持bytecode和纯文本js混合使用:比如a.hbc是bytecode,模块中引用了b.js,b模块是纯文本js。在加载的时候可以先加载a.hbc文件,然后加载b.js文件。可正常执行。
Hermes诸多优点让我们团队非常兴奋,几乎觉得应该立马把JavaScriptCore下掉,更换至Hermes。但随着测试和集成的进行,Hermes带来的问题逐渐显现。
Hermes编译的字节码文件比纯文本js文件增大100%。
携程旅行App的安装包中有20MB(7z压缩后)左右的RN业务代码,如果都编译成bytecode,将会再增加20MB大小,这是无法接受的。另外,动态下发RN增量包时,由于是二进制文件diff,差分效率极低。
为了解决这个问题,我们根据Hermes的特性,转变思路,将Hermes的bytecode编译放到客户端去做,客户端同时存储js和bytecode文件,如果有bytecode编译完成则使用Hermes,否则仍然使用JavaScriptCore。
Hermes开源项目提供了编译bytecode的complieJS方法,但这部分代码没有默认打包到RN的Hermes引擎中,我们稍加整合、封装,通过JNI暴露出来,供业务使用。
拿最大的RN业务包(1100个文件,6.5MB大小),做测试,后台线程执行,小米9 Android10耗时2.49秒;三星S6edge+ android 7.0 耗时6秒。由于bytecode不是必须,因此该耗时尚可接受。
在客户端将纯文本js转换成bytecode之前,我们让Hermes加载纯文本。但实际测试下来,发现Hermes加载纯文本的性能比JavaScriptCore要慢将近30%。主要原因是Hermes删除JIT功能,致使对纯文本js代码运行变慢。
我们对原生RN框架做了大量的优化,缓存使用过的JS执行引擎是优化过程非常重要的一环。
拿门票页面举例来说,如果用户启动App,第一次进入门票业务将会使用一个全新的JavaScript引擎并从磁盘读取文件、加载文件、执行JS代码。用户退出门票页面之后该引擎被缓存,如果用户再一次进入将会使用缓存的引擎,不用重新读取、加载和执行,仅仅需要创建相关JS对象并渲染即可。
遗憾的是,测试Hermes的缓存的时候,我们发现使用缓存的Hermes引擎加载业务代码表现非常一般,甚至某些情况下比第一次加载还要慢。而使用缓存的JavaScriptCore引擎,第二次打开页面的速度与打开纯native页面的速度几乎相当,并且表现相当稳定。
为什么使用缓存的Hermes引擎打开页面速度不理想,可能和Hermes的设计有关,我们还在进一步分析中。
从目前情况来看,在解决缓存问题之前,我们无法在线上版本直接引入Hermes。
解决缓存问题之后,可以采用JavaScriptCore+Hermes双引擎。通过客户端转换bytecode字节码。使用jsc加载优化之前的纯文本js,一旦优化完毕切换至Hermes引擎。
另外如果使用Hermes引擎我们需要充分测试稳定性和兼容性。
Hermes通过预编译字节码的方式提升js执行速度,给了我们新的思路。我们也正在调研JavaScriptCore或者V8的bytecode在移动端的支持度,性能和兼容性。
原文 https://mp.weixin.qq.com/s/BOeuLoZjCdi61P_MhaJT0g
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!