AICon全球人工智能与机器学习技术大会9折特惠中,点击立减¥480! 了解详情
写点什么

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

2019 年 9 月 05 日

加载速度提升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.jsThis 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.hbcThis is Hermes Demo
复制代码


三、Hermes 是如何优化的?

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


3.1 字节码预编译

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


  • 先读取源码文件

  • 解析源代码并转换成字节码(bytecode)

  • 最后执行


在运行时解析源码转换字节码是一种时间浪费,所以 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-clinpm install -g react-native-cli
2.初始化最新react-native工程,最新版为0.60.3react-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节点,比如:eec4dc6cd react-nativenpm 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 Runtimeclass JSCRuntime : public jsi::Runtime
// hermes.h hermes Runtimeclass 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。



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



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



六、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 页面的速度几乎相当,并且表现相当稳定。



为什么使用缓存的 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


2019 年 9 月 05 日 08:002532

评论 1 条评论

发布
用户头像
峰哥牛啊
2021 年 05 月 19 日 17:43
回复
没有更多了
发现更多内容

新晋管理者都会遇到的6个问题

新晋管理者都会遇到的6个问题

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