导读|Hippy应用JS引擎进行异步渲染,在用户从点击到关上首屏可交互过程中会有肯定的耗时,影响用户体验。如何优化这段耗时?腾讯客户端开发工程师李鹏,将介绍QQ浏览器通过切换JS引擎来优化耗时的摸索过程和成果收益。在剖析Hippy耗时瓶颈、比照业界可选引擎计划后,最终QQ浏览器通过抉择应用Hermes引擎、将JS离线生成Bytecode并应用引擎间接加载Bytecode,让首帧耗时优化50%起。心愿本文对面临同样困扰的你有帮忙。
背景
目前QQ浏览器(下简称QB)应用Hippy的业务超过100个,基本上95%的外围业务都是应用Hippy作为首要技术栈来开发。然而跟Native相比较而言,Hippy是应用JS引擎进行异步渲染,在用户从点击到关上首屏可交互过程中会有肯定的耗时,影响用户体验。如何优化耗时,尽量对齐Native体验,想必是许多开发者都在思考优化的事件。
本文次要介绍QQ浏览器通过切换JS引擎来优化耗时的摸索过程和成果收益。本文我将剖析Hippy执行流程及耗时瓶颈、比照业界JS引擎计划,最终抉择应用Hermes引擎。之后剖析将JS离线生成Bytecode,应用引擎间接加载Bytecode的能力。值得一提的是,在业务无需批改一行代码的前提下,Hippy的包加载速度进步80%,首帧耗时优化50%起。上面我将开展讲述。
Hippy业务耗时瓶颈剖析
Hippy整个启动流程依赖JS线程的执行。咱们其实能够将整个过程形象看成一个串行的操作,以QB冷启动首页Feed流,联合线上数据性能监控能够看到如下阶段耗时:
注:TTI = Time To Interact,意思是从业务创立到业务可交互所破费的工夫,因为掂量业务可交互比较复杂,各个业务对可交互的定义不一样,所以这里以首帧上屏为准来掂量;
通过打点剖析失去,用户从关上业务创立RootView开始,到最终首帧上屏总共耗时1488毫秒,其中次要在Module初始化、创立HippyCore(bootstrap.js以及common包执行耗时)、业务包执行耗时上。其中加载执行业务包耗时1303毫秒,占整体TTI的87%。
如果咱们可能优化加载执行业务包的耗时,那么咱们就能够极大的升高TTI。在iOS上Hippy应用的是零碎提供的JavascriptCore引擎来运行JS代码,所以咱们要剖析一下JSC的执行过程。
JavascriptCore执行流程剖析
具体流程:词法剖析,输入tokens;语法分析,生产AST(形象语法树);从AST生成字节码; 通过Low Level解释器执行字节码;应用JIT减速解释执行机器码(带JIT的版本)。
注:本文JSC是指苹果官网提供的JavascriptCore.framework,JSC分带JIT与不带JIT的版本,带JIT的版本目前只有苹果自家的Safari可能应用,公开的JavascriptCore因为平安起因(JIT能够动静执行机器码),理论是不带JIT的版本。上面探讨的也是指不带JIT的JSC版本。
整个流程,在JS代码被解释执行前,绝大部分工夫耗费是在字节码生成上。如果能将Bytecode生成前置缓存起来,每次执行JS的时候间接取缓存的Bytecode,那将会极大升高耗时。然而很惋惜的是,JavascriptCore属于零碎库,并没有提供这个能力。咱们能够思考抉择其余反对Bytecode的引擎替换掉JSC。
可选引擎比照
除了JSC,常见的开源引擎包含V8、QuickJS、Hermes。
注:直出是指反对编译输入Bytecode文件,并且间接运行Bytecode。
Hermes和QuickJS反对直出Bytecode,并且在包大小上比照V8和JSC占优。
1)性能指标比照
以下各项比照取至Linux上各引擎测试数据
包加载耗时速度比照(越低越好)
应用引擎执行业务JS代码,其中JSC和V8均是间接执行JS代码,QuickJS和Hermes是执行Bytecode。
QuickJS一骑绝尘,Hermes紧跟其后,JSC次之,V8最差;
执行效率比照(越高越好)
应用引擎跑一些开源的算法或者出名JS性能库。
V8和JSC性能最好,Hermes次之,QuickJS最差;
内存增量(越低越好)
体现最好的是JSC,其次是Hermes和V8;带JIT的JSC和V8,内存耗费最高;
编译文件大小
掂量编译文件压缩比是为了掂量包下发更新效率,以QB首页Feed流(3.8M左右)举例,JSC和V8均输出原始js文件,QuickJS和Hermes输出JS编译后的Bytecode文件。
JSC和V8压缩比拟高,Hermes和QuickJS压缩比不高,在下发效率上,差于JSC和V8;
2)论断
从执行耗时、执行性能、内存增量、编译文件大小以及整体framework大小5个纬度来剖析看: 带JIT的JSC和V8性能最好,然而加载工夫是最长的,内存耗费也是最多的,包也较大;反对提前预编译的Hermes和QuickJS,加载速度以及内存体现是最好的。
对于进步TTI,加载速度指标最为重要。尽管性能低于JSC和V8,然而对于JS耗时高的操作,能够充分利用modules放在Native去操作;所以基于以上,会优先思考Hermes和QuickJS;
Hermes在性能、内存以及编译包大小上是优于QuickJS的,另外Hermes有Facebook的React Native社区生态反对,相较于QuickJs更新演进更快,所以更偏向应用Hermes来替换JSC。
Hermes引擎调研
1)编译
Hermes尽管是深度集成在React Native里的,然而facebook也将独自的引擎独立进去了,官网地址 仓库地址 编译指南。
依照编译指南编译之后,理论编译的产物只是用于在PC/Mac/Linux运行的Hermes二进制文件。通过这些二进制文件,咱们能够在Terminal里执行JS,以及将JS编译成Bytecode。
# 执行原始JS hermes test.js# 编译并输入以及执行Bytecode hermes -emit-binary -out test.hbc test.js hermes test.hbc
在挪动端上,Hermes也是应用CMake进行编译,并且提供了脚本能够不便输入Android和iOS动静库。具体能够在官网上查看编译指南。
2)运行
Hermes蕴含几个十分重要的构造对象,上面次要讲其中的几个。
Runtime
Hermes应用非常简单,提供了一个Runtime的抽象类,所有的js对象都执行在Runtime对象上,相似JSC的JSContext;派生了HermesRuntime子类来实现所有JS操作。通过静态方法创立一个HermesRuntime对象;
HERMES_EXPORT std::unique_ptr<HermesRuntime> makeHermesRuntime( const ::hermes::vm::RuntimeConfig &runtimeConfig = ::hermes::vm::RuntimeConfig());
同时也提供了一些执行JS的办法
// 执行JS(JS or Bytecode) virtual Value evaluateJavaScript( const std::shared_ptr<const Buffer>& buffer, const std::string& sourceURL) = 0; // 预编译JS virtual std::shared_ptr<const PreparedJavaScript> prepareJavaScript( const std::shared_ptr<const Buffer>& buffer, std::string sourceURL) = 0; // 执行预编译的JS virtual Value evaluatePreparedJavaScript( const std::shared_ptr<const PreparedJavaScript>& js) = 0;
Value
JSC在解决根底数据的时候,所有的类型都是JSValue类型;解决Object是JSObjectRef对象,在Hermes上也有对应的实现;
提供办法判断是什么类型,以及快捷获取类型值,比方:
Bool isStr = value.isString()facebook::jsi::String str = value.asString()
Object
Object对应就是JS的对象,基于Object派生Function以及Array和JSArrayBuffer,同样Object也提供很多办法获取和设置属性。
Runtime提供一个默认的全局对象global, 所有的JS逻辑均运行在默认的global之上。Object也提供很对办法获取属性,比方:
//判断是否有该属性bool hasProperty(Runtime& runtime, const char* name) const;// 获取属性值Value getProperty(Runtime& runtime, const char* name) const;// 获取属性值并转化成objectObject getPropertyAsObject(Runtime& runtime, const char* name) const;
Function
对应JS的Function,提供静态方法创立Function:
// 判断是否有该属性bool hasProperty(Runtime& runtime, const char* name) const;// 获取属性值Value getProperty(Runtime& runtime, const char* name) const;// 获取属性值并转化成objectObject getPropertyAsObject(Runtime& runtime, const char* name) const;
提供实例办法调用:
static Function createFromHostFunction( Runtime& runtime, const jsi::PropNameID& name, unsigned int paramCount, jsi::HostFunctionType func);
同样还有Array,ArrayBuffer,HostObject等等。
通过Runtime,咱们能够获取JS Object、Function,同时咱们也能够创立JS Object、Function,注入给JS,这样就能够实现双向通信。
Hippy2.0架构剖析
1)架构
蕴含三层:
和平台相干的能力扩大比方Module能力和UI组件,以及调用底层HippyCore的接口封装的Bridge和JS Executor层,该层在iOS和Android上别离应用OC和JAVA实现。
HippyCore层,通过napi对不同JS引擎的接口进行接口封装,抹平不同引擎的接口差别,让下层调用通过调用简略的接口实现简单的能力,该层应用C++实现,跨平台。
前端JS SDK层,次要是定义了双向通信的办法函数跟下层进行通信以及性能解决。
另外还包含一些能力,根本是在hippycore层实现。比方C++ Modules, TurboModules等。
咱们须要切换引擎,高低两层其实都不须要特地(大量)批改,外围就是在hippycore层,须要应用hermes将napi定义的接口全副实现一遍,以及同时实现当初曾经有的Abilites。
napi
次要有几种概念:Engine:负责创立VM以及Scope;VM:负责创立治理Ctx,一个VM能够创立一个或者多个Ctx;Ctx:负责创立引擎实例,并封装操作引擎的接口供内部调用;CtxValue:负责封装不同引擎的JS Value;Scope:应用Ctx,执行Hippy根底初始化流程。
Scope
次要负责Hippy根底初始化流程,外围步骤如下:
注入Natives办法
通过给JS注入Native Function办法的形式,让JS能够间接调用终端办法;次要是常见的JS侧CallNative办法均通过此进行散发。
执行JS Native Source Code
Hippy将一部分根底JS SDK代码,通过脚本将JS代码转换成二进制集成在hippycore的C++代码里,在通过Ctx执行这些JS代码。益处是:解决C++ Module跟JS侧代码一致性问题(均应用C++模式加载调用);对于罕用的根底JS的SDK代码,不必打包到根底包里,能够缩小Common包大小,另外职责也拆散。
其中包含C++ Module跟JS对象绑定,以及TurboModule和DynamicImport均在此步骤进行定义实现;
Abilities
C++ Module:不同于Native Module字符串音讯映射和TurboModule HostObject的实现,C++ Module是将HippyCore里标记为导出的C++Module和其函数对应在前端生成一个名字一样的JS对象和办法。Hippy里常见的TimeModule,ContextifyModule均是如此实现。
TurboModule:前有NativeModule,后有C++Module,为什么还有TurboModle?
NativeModule益处是对于一些能力要分端去实现的,两端实现起来比拟不便,然而其是通过字符串映射到终端办法的形式进行调用以及存在JS线程到NativeModule线程切换效率问题。
C++Module的益处就是在JS线程间接调用绑定JS对象和办法执行,效率高,然而裸露的Module是用C++实现,如果散发调用到Native侧,一个是要辨别平台,第二个是散发到下层Java或者OC须要对应的类型转换。
为了解决上述问题,TuroboModule应运而生,兼具JS线程间接调用,并且不同平台能够别离实现本人的Turbo能力,要害是间接应用的引擎提供的HostObject形式实现,相较于C++Module 效率都更高。
Dynamic Import:动静导入能力,答应在JS侧动静加载近程或者本地JS代码,次要应用场景是对于分包加载,缩小主包大小,进步业务加载包速度;最终实现也是通过C++Module ContextifyModule的LoadUntrustedContent办法来执行远端或者本地JS代码并返回给JS侧。
HippyCore异样解决:JS引擎接口异样,不同引擎异样不同(JSI Exception);Native异样,次要是Native侧的代码调用以及JS办法注入实现异样。
JSC引擎和V8解决逻辑不太一样,JSC的JSI接口会将Exception通过参数传递进去,V8是通过在调用上下文初始化TryCatch对象,对异样进行捕捉。
所以对于JSC的JS异样,只须要解决接口的Exception就行;V8解决TryCatch对象捕捉的异样就能够。
SValueRef js_error = nullptr; JSValueRef value_ref = JSObjectGetProperty(context_, global_obj, name_ref, &js_error); bool is_str = JSValueIsString(context_, value_ref); JSStringRelease(name_ref);
Native异样个别就是平台相干的异样,比方OC就是NSException,在双向通信以及各种JS接口注入实现处加Try-Catch进行捕捉。
2)总结
通过以上架构剖析,Hippy整个实现流程都曾经变得十分清晰,咱们能够应用Hermes的能力将上述能力均实现一下。
Hermes接入比照
1)性能
基于曾经上线的业务性能统计数据(数据取至12月12日),比照如下:
能够看到包加载执行耗时曾经被彻底打下来了(70-80%幅度),进而极大升高了首帧耗时。
另外通过线上业务大盘整体耗时曲线图能够更直观看到成果(大部分业务没有全量,所以还会有继续降落的趋势):
2)内存
在滑动雷同的的List Item的状况下,Hippy Hermes和JSC的内存增量差异不大。依据官网文档介绍Hermes应该是略优于JSC的,所以这里不排除Hippy或者前端SDK还有优化空间。
3)Crash
Hippy的JSC相干的Crash率较高,比拟难批改。Hermes也有肯定的crash,然而从目前的比照来看,数量级较JSC少很多。以12月12日,iOS 13.4.0.5401版本的数据比照来看,Hermes的Crash率为JSC的50%,也就是说如果切换到Hermes上的话,相干引擎的Crash会降落一半。
JSC Crash关键词:jscctx/HippyJSCExecutor Hermes Crash关键词:hermes/HippyHermesExecutor
瞻望
目前Hermes曾经在QB iOS版本上上线。业务接入老本非常低,无需批改一行代码,只须要打包的时候应用插件,输入Bytecode文件即可。接入上线的业务曾经遍布信息流、浏览、商业、搜寻等各个业务场景。
当然,还有很多事件能够继续做以继续晋升性能: Android接入,比照V8性能,曾经靠近实现(比照V8,在低中端手机上有近50%的性能晋升)。Hermes调试能力,能够应用Hermes在Chrome上调试JS代码。 基于Hermes的内存调试诊断工具。本文不开展赘述,欢送各位开发者交换摸索~
通过接入Hermes,能够让业务更多的关注在JS业务逻辑里,让前置SDK流程的耗时不再是性能瓶颈。心愿本文能给你灵感。
公众号回复“性能优化“,查看作者举荐的更多文章
腾讯工程师技术干货中转:
1、H5开屏从龟速到闪电,企微是如何做到的
2、内存泄露?腾讯工程师2个压箱底的办法和工具
3、全网首次揭秘:微秒级“复活”网络的HARP协定及其关键技术
4、万字避坑指南!C++的缺点与思考(下)