引言
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 是如何优化的?
主流 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 从新创建工程集成
4.2 从源码集成
4.3 Hermes 集成过程分析
分析 react-native react.gradle 源码可以看到,如果打开了 Hermes 开关,会在原先打包 RN 代码的 bundleXXXJsAndAsset task 后面追加执行一段 Hermes 转换命令: hermes --emit-binary -out xxx。
4.4 执行过程分析
为了进一步抽象 JavaScript 执行层,RN 底层创建了 JSExecutor 和 Runtime 接口,并把大部分业务逻辑放到了实现了 JSExecutor 的 JSIExcutor.cpp 中。对于 JavaScript 执行引擎来说只需要实现 Runtime 接口即可对接 RN 框架。
JavaScriptCore 的 Runtime 实现类是 JSCRuntime。相应的,此次 Hermes 升级,底层创建了 HermesRuntime。
每一种 JSExecutor 都提供了创建类 XXXExecutorFactory 来创建相应实例,并且提供了相应的 Java 对象。
RN 框架在初始化 ReactInstanceManager 的时候需要传入 JavaScriptExecutorFactory。如果要切换 JavaScript 执行引擎只需要在 ReactInstanceManager 创建的时候做控制即可。
官方的控制流程是,优先加载 jscexecutorso,如果成功则使用 JSCRuntime,否则使用 HermesRuntime。
由此可见无论是对于 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
评论 1 条评论