作者:pennyli,騰訊PCG客戶(hù)端開(kāi)發(fā)工程師

背景

目前QQ瀏覽器(下簡(jiǎn)稱(chēng)QB)使用Hippy的業(yè)務(wù)超過(guò)100個(gè),基本上95%的核心業(yè)務(wù)都是使用Hippy作為首要技術(shù)棧來(lái)開(kāi)發(fā)。但是跟Native相比較而言,Hippy是使用JS引擎進(jìn)行異步渲染,在用戶(hù)從點(diǎn)擊到打開(kāi)首屏可交互過(guò)程中會(huì)有一定的耗時(shí),影響用戶(hù)體驗(yàn)。如何優(yōu)化這段耗時(shí),盡量對(duì)齊Native體驗(yàn),想必是每個(gè)團(tuán)隊(duì)需要思考優(yōu)化的事情。

本文主要介紹QQ瀏覽器通過(guò)切換JS引擎來(lái)優(yōu)化耗時(shí)的探索過(guò)程和效果收益,主要包含:通過(guò)分析Hippy執(zhí)行流程及耗時(shí)瓶頸,對(duì)比業(yè)界JS引擎方案,選擇使用Hermes引擎,將JS離線(xiàn)生成Bytecode,使用引擎直接加載Bytecode的能力,在業(yè)務(wù)無(wú)需修改一行代碼的前提下,讓Hippy的包加載速度提高80%,首幀耗時(shí)優(yōu)化50%起。

Hippy業(yè)務(wù)耗時(shí)瓶頸分析

Hippy整個(gè)啟動(dòng)流程,依賴(lài)JS線(xiàn)程的執(zhí)行,我們其實(shí)可以將整個(gè)過(guò)程抽象看成一個(gè)串行的操作,以QB冷啟動(dòng)首頁(yè)Feed流,結(jié)合線(xiàn)上數(shù)據(jù)性能監(jiān)控可以看到如下階段耗時(shí):

qq網(wǎng)頁(yè)webqq,QQ網(wǎng)頁(yè)登錄?

注:TTI = Time To Interact,意思是從業(yè)務(wù)創(chuàng)建到業(yè)務(wù)可交互所花費(fèi)的時(shí)間,因?yàn)楹饬繕I(yè)務(wù)可交互比較復(fù)雜,各個(gè)業(yè)務(wù)對(duì)可交互的定義不一樣,所以這里以首幀上屏為準(zhǔn)來(lái)衡量;

通過(guò)打點(diǎn)分析得到,用戶(hù)從打開(kāi)業(yè)務(wù)創(chuàng)建RootView開(kāi)始,到最終首幀上屏總共耗時(shí)1488毫秒,其中主要在Module初始化、創(chuàng)建HippyCore(bootstrap.js以及common包執(zhí)行耗時(shí))、業(yè)務(wù)包執(zhí)行耗時(shí)上;其中加載執(zhí)行業(yè)務(wù)包耗時(shí)1303毫秒,占整體TTI的87%。

如果我們能夠優(yōu)化加載執(zhí)行業(yè)務(wù)包的耗時(shí),那么我們就可以極大的降低TTI。在iOS上Hippy使用的是系統(tǒng)提供的JAVAscriptCore引擎來(lái)運(yùn)行JS代碼,所以我們要分析一下JSC的執(zhí)行過(guò)程。

JavascriptCore執(zhí)行流程分析

具體流程:

1. 詞法分析,輸出tokens;

2. 語(yǔ)法分析,生產(chǎn)AST(抽象語(yǔ)法樹(shù));

3. 從AST生成字節(jié)碼;

4. 通過(guò)Low Level解釋器執(zhí)行字節(jié)碼;

5. 使用JIT加速解釋執(zhí)行機(jī)器碼;(帶JIT的版本)

qq網(wǎng)頁(yè)webqq,QQ網(wǎng)頁(yè)登錄?

注:本文JSC是指蘋(píng)果官方提供的JavascriptCore.framework,JSC分帶JIT與不帶JIT的版本,帶JIT的版本目前只有蘋(píng)果自家的Safari能夠使用,公開(kāi)的JavascriptCore因?yàn)榘踩颍↗IT可以動(dòng)態(tài)執(zhí)行機(jī)器碼),實(shí)際是不帶JIT的版本。下面討論的也是指不帶JIT的JSC版本。

整個(gè)流程,在JS代碼被解釋執(zhí)行前,絕大部分時(shí)間消耗是在字節(jié)碼生成上,如果能將Bytecode生成前置,緩存起來(lái),每次執(zhí)行JS的時(shí)候,直接取緩存的Bytecode,那將會(huì)極大降低耗時(shí),但是很可惜的是,JavascriptCore是屬于系統(tǒng)庫(kù),并沒(méi)有提供這個(gè)能力。我們可以考慮選擇其他支持Bytecode的引擎替換掉JSC。

可選引擎對(duì)比

除了JSC,常見(jiàn)的開(kāi)源引擎包括V8、QuickJS、Hermes。

JS引擎

是否支持Bytecode

SDK大小

是否開(kāi)源

作者

JavascriptCore

0

Apple

V8

(僅僅是支持CodeCache,不支持直出Bytecode)

8-10M

Google

QuickJS

1M

Bellard

Hermes

3M

facebook

注:直出是指支持編譯輸出Bytecode文件,并且直接運(yùn)行Bytecode。

Hermes和QuickJS支持直出Bytecode,并且在包大小上對(duì)比V8和JSC占優(yōu)。

性能指標(biāo)對(duì)比

以下各項(xiàng)對(duì)比取至Linux上各引擎測(cè)試數(shù)據(jù)

包加載耗時(shí)速度對(duì)比(越低越好)

使用引擎執(zhí)行業(yè)務(wù)JS代碼,其中JSC和V8均是直接執(zhí)行JS代碼,QuickJS和Hermes是執(zhí)行Bytecode。

qq網(wǎng)頁(yè)webqq,QQ網(wǎng)頁(yè)登錄?

QuickJS一騎絕塵,Hermes緊跟其后,JSC次之,V8最差;

執(zhí)行效率對(duì)比(越高越好)

使用引擎跑一些開(kāi)源的算法或者知名JS功能庫(kù)。

qq網(wǎng)頁(yè)webqq,QQ網(wǎng)頁(yè)登錄?

qq網(wǎng)頁(yè)webqq,QQ網(wǎng)頁(yè)登錄?

V8和JSC性能最好,Hermes次之,QuickJS最差;

內(nèi)存增量(越低越好)

qq網(wǎng)頁(yè)webqq,QQ網(wǎng)頁(yè)登錄?

qq網(wǎng)頁(yè)webqq,QQ網(wǎng)頁(yè)登錄?

表現(xiàn)最好的是JSC,其次是Hermes和V8;帶JIT的JSC和V8,內(nèi)存消耗最高;

編譯文件大小

衡量編譯文件壓縮比是為了衡量包下發(fā)更新效率,以QB首頁(yè)Feed流(3.8M左右)舉例,JSC和V8均輸入原始js文件,QuickJS和Hermes輸入JS編譯后的Bytecode文件。

qq網(wǎng)頁(yè)webqq,QQ網(wǎng)頁(yè)登錄?

JSC和V8壓縮比較高,Hermes和QuickJS壓縮比不高,在下發(fā)效率上,差于JSC和V8;

結(jié)論

從執(zhí)行耗時(shí)、執(zhí)行性能、內(nèi)存增量、編譯文件大小以及整體framework大小5個(gè)緯度來(lái)分析看:

1. 帶JIT的JSC和V8性能最好,但是加載時(shí)間是最長(zhǎng)的,內(nèi)存消耗也是最多的,包也較大;

2. 支持提前預(yù)編譯的Hermes和QuickJS,加載速度以及內(nèi)存表現(xiàn)是最好的;

對(duì)于提高TTI,加載速度指標(biāo)最為重要。雖然性能低于JSC和V8,但是對(duì)于JS耗時(shí)高的操作,可以充分利用modules放在Native去操作;所以基于以上,會(huì)優(yōu)先考慮Hermes和QuickJS;

Hermes在性能、內(nèi)存以及編譯包大小上是優(yōu)于QuickJS的,另外Hermes有Facebook的React Native社區(qū)生態(tài)支持,相較于QuickJs更新演進(jìn)更快,所以更傾向使用Hermes來(lái)替換JSC。

Hermes引擎調(diào)研

編譯

Hermes雖然是深度集成在React Native里的,但是facebook也將單獨(dú)的引擎獨(dú)立出來(lái)了,官網(wǎng)地址 倉(cāng)庫(kù)地址 編譯指南。

按照編譯指南編譯之后,實(shí)際編譯的產(chǎn)物只是用于在PC/Mac/Linux運(yùn)行的Hermes二進(jìn)制文件。通過(guò)這些二進(jìn)制文件,我們可以

在Terminal里執(zhí)行JS,以及將JS編譯成Bytecode。

# 執(zhí)行原始JS 
hermes test.js
# 編譯并輸出以及執(zhí)行Bytecode 
hermes -emit-binary -out test.hbc test.js hermes test.hbc

在移動(dòng)端上,Hermes也是使用CMake進(jìn)行編譯,并且提供了腳本可以方便輸出Android和iOS動(dòng)態(tài)庫(kù)。具體可以在官網(wǎng)上查看編譯指南。

運(yùn)行

Hermes包含幾個(gè)非常重要的結(jié)構(gòu)對(duì)象,下面主要講其中的幾個(gè)。

runtime

Hermes使用非常簡(jiǎn)單,提供了一個(gè)Runtime的抽象類(lèi),所有的js對(duì)象都執(zhí)行在Runtime對(duì)象上,類(lèi)似JSC的JSContext;派生了HermesRuntime子類(lèi)來(lái)實(shí)現(xiàn)所有JS操作。通過(guò)靜態(tài)方法創(chuàng)建一個(gè)HermesRuntime對(duì)象;

HERMES_EXPORT std::unique_ptr<HermesRuntime> makeHermesRuntime(
    const ::hermes::vm::RuntimeConfig &runtimeConfig =
        ::hermes::vm::RuntimeConfig());

同時(shí)也提供了一些執(zhí)行JS的方法

  // 執(zhí)行JS(JS or Bytecode)
  virtual Value evaluateJavaScript(
      const std::shared_ptr<const Buffer>& buffer,
      const std::string& sourceURL) = 0;
  
  // 預(yù)編譯JS
  virtual std::shared_ptr<const PreparedJavaScript> prepareJavaScript(
      const std::shared_ptr<const Buffer>& buffer,
      std::string sourceURL) = 0;
  
  // 執(zhí)行預(yù)編譯的JS
  virtual Value evaluatePreparedJavaScript(
      const std::shared_ptr<const PreparedJavaScript>& js) = 0;

Value

JSC在處理基礎(chǔ)數(shù)據(jù)的時(shí)候,所有的類(lèi)型都是JSValue類(lèi)型;處理Object是JSObjectRef對(duì)象,在Hermes上也有對(duì)應(yīng)的實(shí)現(xiàn);

qq網(wǎng)頁(yè)webqq,QQ網(wǎng)頁(yè)登錄?

提供方法判斷是什么類(lèi)型,以及快捷獲取類(lèi)型值,比如:

bool isStr = value.isString()
facebook::jsi::String str = value.asString()

Object

Object對(duì)應(yīng)就是JS的對(duì)象,基于Object派生Function以及Array和JSArrayBuffer,同樣Object也提供很多方法獲取和設(shè)置屬性;

Runtime提供一個(gè)默認(rèn)的全局對(duì)象global, 所有的JS邏輯均運(yùn)行在默認(rèn)的global之上。Object也提供很對(duì)方法獲取屬性,比如:

// 判斷是否有該屬性
boolhasProperty(Runtime&runtime,constchar*name)const;

// 獲取屬性值
ValuegetProperty(Runtime&runtime,constchar*name)const;

// 獲取屬性值并轉(zhuǎn)化成
objectObjectgetPropertyAsObject(Runtime&runtime,constchar*name)const;

Function

對(duì)應(yīng)JS的Function,提供靜態(tài)方法創(chuàng)建Function:

static Function createFromHostFunction(
      Runtime& runtime,
      const jsi::PropNameID& name,
      unsigned int paramCount,
      jsi::HostFunctionType func);

提供實(shí)例方法調(diào)用:

auto func = Function::createFromHostFunction(...)
func.call(...)

同樣還有Array,ArrayBuffer,HostObject等等。

通過(guò)Runtime,我們可以獲取JS Object、Function,同時(shí)我們也可以創(chuàng)建JS Object、Function,注入給JS,這樣就可以實(shí)現(xiàn)雙向通信。

Hippy2.0架構(gòu)分析

架構(gòu)

qq網(wǎng)頁(yè)webqq,QQ網(wǎng)頁(yè)登錄?

包含三層:

1. 和平臺(tái)相關(guān)的能力擴(kuò)展比如Module能力和UI組件,以及調(diào)用底層HippyCore的接口封裝的Bridge和JS Executor層,該層在iOS和Android上分別使用OC和Java實(shí)現(xiàn);

2. HippyCore層,通過(guò)napi對(duì)不同JS引擎的接口進(jìn)行接口封裝,抹平不同引擎的接口差異,讓上層調(diào)用通過(guò)調(diào)用簡(jiǎn)單的接口實(shí)現(xiàn)復(fù)雜的能力,該層使用C++實(shí)現(xiàn),跨平臺(tái)。

3. 前端JS SDK層,主要是定義了雙向通信的方法函數(shù)跟上層進(jìn)行通信以及功能處理。

另外還包括一些能力,基本是在hippycore層實(shí)現(xiàn)。比如C++ Modules, TurboModules等。

我們需要切換引擎,上下兩層其實(shí)都不需要特別(大量)修改,核心就是在hippycore層,需要使用hermes將napi定義的接口全部實(shí)現(xiàn)一遍,以及同時(shí)實(shí)現(xiàn)現(xiàn)在已經(jīng)有的Abilites。

napi

主要有幾種概念

  1. Engine:負(fù)責(zé)創(chuàng)建VM以及Scope;
  2. VM:負(fù)責(zé)創(chuàng)建管理Ctx,一個(gè)VM可以創(chuàng)建一個(gè)或者多個(gè)Ctx;
  3. Ctx:負(fù)責(zé)創(chuàng)建引擎實(shí)例,并封裝操作引擎的接口供外部調(diào)用;
  4. CtxValue:負(fù)責(zé)封裝不同引擎的JS Value;
  5. Scope:使用Ctx,執(zhí)行Hippy基礎(chǔ)初始化流程;

Scope

主要負(fù)責(zé)Hippy基礎(chǔ)初始化流程,核心步驟如下:

qq網(wǎng)頁(yè)webqq,QQ網(wǎng)頁(yè)登錄?

注入Natives方法

通過(guò)給JS注入Native Function方法的方式,讓JS可以直接調(diào)用終端方法;主要是常見(jiàn)的JS側(cè)CallNative方法均通過(guò)此進(jìn)行分發(fā)。

執(zhí)行JS Native Source Code

Hippy將一部分基礎(chǔ)JS SDK代碼,通過(guò)腳本將JS代碼轉(zhuǎn)換成二進(jìn)制集成在hippycore的C++代碼里,在通過(guò)Ctx執(zhí)行這些JS代碼。好處是:

1. 解決C++ Module跟JS側(cè)代碼一致性問(wèn)題(均使用C++形式加載調(diào)用)。

2. 對(duì)于常用的基礎(chǔ)JS的SDK代碼,不用打包到基礎(chǔ)包里,可以減少Common包大小,另外職責(zé)也分離。

其中包括C++ Module跟JS對(duì)象綁定,以及TurboModule和DynamicImport均在此步驟進(jìn)行定義實(shí)現(xiàn);

Abilities

C++ Module

不同于Native Module字符串消息映射和TurboModule HostObject的實(shí)現(xiàn),C++ Module是將HippyCore里標(biāo)記為導(dǎo)出的C++Module和其函數(shù)對(duì)應(yīng)在前端生成一個(gè)名字一樣的JS對(duì)象和方法。Hippy里常見(jiàn)的TimeModule,ContextifyModule均是如此實(shí)現(xiàn)。

TurboModule

前有NativeModule,后有C++Module,為什么還有TurboModle?

NativeModule好處是對(duì)于一些能力要分端去實(shí)現(xiàn)的,兩端實(shí)現(xiàn)起來(lái)比較方便,但是其是通過(guò)字符串映射到終端方法的方式進(jìn)行調(diào)用以及存在JS線(xiàn)程到NativeModule線(xiàn)程切換效率問(wèn)題;

C++Module的好處就是在JS線(xiàn)程直接調(diào)用綁定JS對(duì)象和方法執(zhí)行,效率高,但是暴露的Module是用C++實(shí)現(xiàn),如果分發(fā)調(diào)用到Native側(cè),一個(gè)是要區(qū)分平臺(tái),第二個(gè)是分發(fā)到上層Java或者OC需要對(duì)應(yīng)的類(lèi)型轉(zhuǎn)換。

為了解決上述問(wèn)題,TuroboModule應(yīng)運(yùn)而生,兼具JS線(xiàn)程直接調(diào)用,并且不同平臺(tái)可以分別實(shí)現(xiàn)自己的Turbo能力,關(guān)鍵是直接使用的引擎提供的HostObject方式實(shí)現(xiàn),相較于C++Module 效率都更高。

Dynamic Import

動(dòng)態(tài)導(dǎo)入能力,容許在JS側(cè)動(dòng)態(tài)加載遠(yuǎn)程或者本地JS代碼,主要使用場(chǎng)景是對(duì)于分包加載,減少主包大小,提高業(yè)務(wù)加載包速度;最終實(shí)現(xiàn)也是通過(guò)C++Module ContextifyModule的LoadUntrustedContent方法來(lái)執(zhí)行遠(yuǎn)端或者本地JS代碼并返回給JS側(cè)。

HippyCore異常處理

分兩種異常

  1. JS引擎接口異常,不同引擎異常不同(JSI Exception);
  2. Native異常,主要是Native側(cè)的代碼調(diào)用以及JS方法注入實(shí)現(xiàn)異常。

JS引擎接口報(bào)的異常

JSC引擎和V8處理邏輯不太一樣,JSC的JSI接口會(huì)將Exception通過(guò)參數(shù)傳遞出來(lái),V8是通過(guò)在調(diào)用上下文初始化TryCatch對(duì)象,對(duì)異常進(jìn)行捕獲;

所以對(duì)于JSC的JS異常,只需要處理接口的Exception就行;V8處理TryCatch對(duì)象捕獲的異常就可以;

JSValueRef 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異常

一般就是平臺(tái)相關(guān)的異常,比如OC就是NSException,在雙向通信以及各種JS接口注入實(shí)現(xiàn)處加Try-Catch進(jìn)行捕獲。

總結(jié)

通過(guò)以上架構(gòu)分析,Hippy整個(gè)實(shí)現(xiàn)流程都已經(jīng)變得非常清晰,我們可以使用Hermes的能力將上述能力均實(shí)現(xiàn)一下。

Hermes接入

本來(lái)是可以很詳細(xì)介紹的,但是受限于篇幅不宜過(guò)長(zhǎng),后面在開(kāi)一篇來(lái)講。

Hermes接入對(duì)比

性能

基于已經(jīng)上線(xiàn)的業(yè)務(wù)性能統(tǒng)計(jì)數(shù)據(jù)(數(shù)據(jù)取至12月12日),對(duì)比如下:

qq網(wǎng)頁(yè)webqq,QQ網(wǎng)頁(yè)登錄?

可以看到包加載執(zhí)行耗時(shí)已經(jīng)被徹底打下來(lái)了(70-80%幅度),進(jìn)而極大降低了首幀耗時(shí)。

另外通過(guò)線(xiàn)上業(yè)務(wù)大盤(pán)整體耗時(shí)曲線(xiàn)圖可以更直觀(guān)看到效果(大部分業(yè)務(wù)沒(méi)有全量,所以還會(huì)有持續(xù)下降的趨勢(shì)):

qq網(wǎng)頁(yè)webqq,QQ網(wǎng)頁(yè)登錄?

qq網(wǎng)頁(yè)webqq,QQ網(wǎng)頁(yè)登錄?

內(nèi)存

在滑動(dòng)相同的的List Item的情況下,Hippy Hermes和JSC的內(nèi)存增量差別不大。根據(jù)官方文檔介紹Hermes應(yīng)該是略?xún)?yōu)于JSC的,所以這里不排除Hippy或者前端SDK還有優(yōu)化空間。

Crash

Hippy的JSC相關(guān)的Crash率較高,比較難修改。 Hermes也有一定的crash,但是從目前的對(duì)比來(lái)看,數(shù)量級(jí)較JSC少很多。以12月12日,iOS 13.4.0.5401版本的數(shù)據(jù)對(duì)比來(lái)看,Hermes的Crash率為JSC的50%,也就是說(shuō)如果切換到Hermes上的話(huà),相關(guān)引擎的Crash會(huì)下降一半。

qq網(wǎng)頁(yè)webqq,QQ網(wǎng)頁(yè)登錄?

JSC Crash關(guān)鍵詞:jscctx/HippyJSCExecutor Hermes Crash關(guān)鍵詞:hermes/HippyHermesExecutor

計(jì)劃的事情

目前Hermes已經(jīng)在QB iOS版本上上線(xiàn),業(yè)務(wù)接入成本非常低,無(wú)需修改一行代碼,只需要打包的時(shí)候使用插件,輸出Bytecode文件即可。接入上線(xiàn)的業(yè)務(wù)已經(jīng)遍布信息流、閱讀、商業(yè)、搜索等各個(gè)業(yè)務(wù)場(chǎng)景。

剩余重要的事情主要有:

1. Android接入,對(duì)比V8性能,已經(jīng)接近完成(對(duì)比V8,在低中端手機(jī)上有近50%的性能提升)。

2. Hermes調(diào)試能力,可以使用Hermes在Chrome上調(diào)試JS代碼,Coming Soon!

3. 基于Hermes的內(nèi)存調(diào)試診斷工具;

最后

通過(guò)接入Hermes,可以讓業(yè)務(wù)更多的關(guān)注在JS業(yè)務(wù)邏輯里,讓前置SDK流程的耗時(shí)不再是性能瓶頸。

版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶(hù)自發(fā)貢獻(xiàn),該文觀(guān)點(diǎn)僅代表作者本人。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權(quán)/違法違規(guī)的內(nèi)容, 請(qǐng)發(fā)送郵件至 sumchina520@foxmail.com 舉報(bào),一經(jīng)查實(shí),本站將立刻刪除。

相關(guān)新聞

聯(lián)系我們

聯(lián)系我們

400-9010-860

在線(xiàn)咨詢(xún):點(diǎn)擊這里給我發(fā)消息

微信:85018612

商夢(mèng)建站客服

工作時(shí)間:周一至周六

9:00-18:30,節(jié)假日休息

關(guān)注微信
關(guān)注微信
分享本頁(yè)
返回頂部