加载速度提升 15%,携程对 RN 新一代 JS 引擎 Hermes 的调研

阅读数:1473 2019 年 9 月 5 日 08:00

加载速度提升15%,携程对RN新一代JS引擎Hermes的调研

引言

Facebook 在 ChainReact2019 大会上正式推出了新一代 JavaScript 执行引擎 Hermes。Hermes 是个轻量级的 JS 引擎,专门对 Android 上运行 ReactNative 进行了优化。我们第一时间在 CRN 项目中集成了 Hermes, 并做了深度调研。

一、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 的使用和实测性能数据。

二、快速上手 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

三、Hermes 是如何优化的?

主流 JavaScript 引擎,例如 JSC、V8、SpiderMonkey 等几乎都是为了桌面端浏览器服务的,Hermes 针对移动终端设备的特点做了一些优化,其中最重要的我们认为是以下两点:

3.1 字节码预编译

现代主流的 JavaScript 引擎在执行一段 js 代码的大概流程是:

  • 先读取源码文件
  • 解析源代码并转换成字节码(bytecode)
  • 最后执行

在运行时解析源码转换字节码是一种时间浪费,所以 Hermes 选择预编译的方式在编译期间生成字节码。这样做一方面避免了不必要的转换时间,另一方面多出的时间可以用来优化字节码,从而提高执行效率。

加载速度提升15%,携程对RN新一代JS引擎Hermes的调研

3.2 放弃 JIT

为了加快执行效率,现在主流的 JavaScript 引擎都会使用一个 JIT 编译器在运行时通过转换成机器码的方式优化 JS 代码。Faceback 团队认为 JIT 编译器有主要俩个问题:

  • 要在启动时候预热,对启动时间有影响;
  • 会增加引擎 size 大小和运行时内存消耗;

基于这俩点对性能指标的影响,Faceback 团队决定不实现 JIT 编译器。

这里所谓放弃 JIT,有两点需要再解释一下:

  • 纯文本 JS 代码执行效率降低。放弃 JIT,是指放弃运行时 Hermes 引擎对纯文本 JS 代码的编译优化。我们的验证数据也表面,纯文本的 JS 代码执行,Hermes 引擎明显比 JavaScriptCore 慢。
  • 对 RN 代码的动态性无影响。由于 Hermes 仍然可以执行纯文本的 JS 代码,并且可以支持动态读取 bytecode, 因此对 RN 的动态性并无影响。

四、如何集成 Hermes?

4.1 从新创建工程集成

复制代码
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

4.2 从源码集成

复制代码
git clone https://github.com/facebook/react-native.git // 需要切换到 Hermes release 节点,比如:eec4dc6
cd react-native
npm install
./gradlew :RNTester:android:app:installHermesRelease // 使用生产环境 hermes

4.3 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)
}
...

4.4 执行过程分析

为了进一步抽象 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,JavaScriptCore,V8 的对比

通过上面的 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。

加载速度提升15%,携程对RN新一代JS引擎Hermes的调研

  • 内存:拿 RNTester 工程测试进入 RN 页面滑动进入若干页面并退出之后,内存的波动情况比较可以看到,V8 和 Hermes 内存增长要更加平滑。

加载速度提升15%,携程对RN新一代JS引擎Hermes的调研

  • CPU:拿 RNTester 工程测试进入 RN 页面滑动进入若干页面并退出之后,对比 CPU 波动情况。Hermes 明显好于 V8 和 JavaScriptCore。

加载速度提升15%,携程对RN新一代JS引擎Hermes的调研

六、Hermes 引擎的动态性

另外通过我们的测试,Hermes 在执行字节码和文本 JS 上有一些很有意思的特性,这些特性让升级成本变得非常低:

  • Hermes 支持执行纯文本的 js
  • 支持动态加载纯文本 js 或者 bytecode
  • 支持 bytecode 和纯文本 js 混合使用:比如 a.hbc 是 bytecode,模块中引用了 b.js,b 模块是纯文本 js。在加载的时候可以先加载 a.hbc 文件,然后加载 b.js 文件。可正常执行。

七、Hermes 目前的问题

Hermes 诸多优点让我们团队非常兴奋,几乎觉得应该立马把 JavaScriptCore 下掉,更换至 Hermes。但随着测试和集成的进行,Hermes 带来的问题逐渐显现。

7.1 bytecode 文件占用 size 过大问题

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 不是必须,因此该耗时尚可接受。

7.2 执行纯文本 js 耗时长

在客户端将纯文本 js 转换成 bytecode 之前,我们让 Hermes 加载纯文本。但实际测试下来,发现 Hermes 加载纯文本的性能比 JavaScriptCore 要慢将近 30%。主要原因是 Hermes 删除 JIT 功能,致使对纯文本 js 代码运行变慢。

7.3 缓存问题

我们对原生 RN 框架做了大量的优化,缓存使用过的 JS 执行引擎是优化过程非常重要的一环。

拿门票页面举例来说,如果用户启动 App,第一次进入门票业务将会使用一个全新的 JavaScript 引擎并从磁盘读取文件、加载文件、执行 JS 代码。用户退出门票页面之后该引擎被缓存,如果用户再一次进入将会使用缓存的引擎,不用重新读取、加载和执行,仅仅需要创建相关 JS 对象并渲染即可。

遗憾的是,测试 Hermes 的缓存的时候,我们发现使用缓存的 Hermes 引擎加载业务代码表现非常一般,甚至某些情况下比第一次加载还要慢。而使用缓存的 JavaScriptCore 引擎,第二次打开页面的速度与打开纯 native 页面的速度几乎相当,并且表现相当稳定。

加载速度提升15%,携程对RN新一代JS引擎Hermes的调研

为什么使用缓存的 Hermes 引擎打开页面速度不理想,可能和 Hermes 的设计有关,我们还在进一步分析中。

八、总结与展望

  • 从目前情况来看,在解决缓存问题之前,我们无法在线上版本直接引入 Hermes。
  • 解决缓存问题之后,可以采用 JavaScriptCore+Hermes 双引擎。通过客户端转换 bytecode 字节码。使用 jsc 加载优化之前的纯文本 js,一旦优化完毕切换至 Hermes 引擎。
  • 另外如果使用 Hermes 引擎我们需要充分测试稳定性和兼容性。
  • Hermes 通过预编译字节码的方式提升 js 执行速度,给了我们新的思路。我们也正在调研 JavaScriptCore 或者 V8 的 bytecode 在移动端的支持度,性能和兼容性。

作者介绍

储贻锋,携程无线平台研发部基础框架组资深 Android 研发,目前主要负责 CRN Android 端和携程 Android 基础架构的维护与开发工作。

本文转载自公众号携程技术中心(ID:ctriptech)

原文链接

https://mp.weixin.qq.com/s/BOeuLoZjCdi61P_MhaJT0g

评论

发布