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

背景

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

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

Hippy業(yè)務耗時瓶頸分析

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

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

注:TTI = Time To Interact,意思是從業(yè)務創(chuàng)建到業(yè)務可交互所花費的時間,因為衡量業(yè)務可交互比較復雜,各個業(yè)務對可交互的定義不一樣,所以這里以首幀上屏為準來衡量;

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

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

JavascriptCore執(zhí)行流程分析

具體流程:

1. 詞法分析,輸出tokens;

2. 語法分析,生產AST(抽象語法樹);

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

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

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

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

注:本文JSC是指蘋果官方提供的JavascriptCore.framework,JSC分帶JIT與不帶JIT的版本,帶JIT的版本目前只有蘋果自家的Safari能夠使用,公開的JavascriptCore因為安全原因(JIT可以動態(tài)執(zhí)行機器碼),實際是不帶JIT的版本。下面討論的也是指不帶JIT的JSC版本。

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

可選引擎對比

除了JSC,常見的開源引擎包括V8、QuickJS、Hermes。

JS引擎

是否支持Bytecode

SDK大小

是否開源

作者

JavascriptCore

0

Apple

V8

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

8-10M

Google

QuickJS

1M

Bellard

Hermes

3M

facebook

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

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

性能指標對比

以下各項對比取至Linux上各引擎測試數(shù)據(jù)

包加載耗時速度對比(越低越好)

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

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

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

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

使用引擎跑一些開源的算法或者知名JS功能庫。

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

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

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

內存增量(越低越好)

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

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

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

編譯文件大小

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

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

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

結論

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

1. 帶JIT的JSC和V8性能最好,但是加載時間是最長的,內存消耗也是最多的,包也較大;

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

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

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

Hermes引擎調研

編譯

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

按照編譯指南編譯之后,實際編譯的產物只是用于在PC/Mac/Linux運行的Hermes二進制文件。通過這些二進制文件,我們可以

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

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

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

運行

Hermes包含幾個非常重要的結構對象,下面主要講其中的幾個。

runtime

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

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

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

  // 執(zhí)行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;
  
  // 執(zhí)行預編譯的JS
  virtual Value evaluatePreparedJavaScript(
      const std::shared_ptr<const PreparedJavaScript>& js) = 0;

Value

JSC在處理基礎數(shù)據(jù)的時候,所有的類型都是JSValue類型;處理Object是JSObjectRef對象,在Hermes上也有對應的實現(xiàn);

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

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

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

Object

Object對應就是JS的對象,基于Object派生Function以及Array和JSArrayBuffer,同樣Object也提供很多方法獲取和設置屬性;

Runtime提供一個默認的全局對象global, 所有的JS邏輯均運行在默認的global之上。Object也提供很對方法獲取屬性,比如:

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

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

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

Function

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

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

提供實例方法調用:

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

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

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

Hippy2.0架構分析

架構

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

包含三層:

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

2. HippyCore層,通過napi對不同JS引擎的接口進行接口封裝,抹平不同引擎的接口差異,讓上層調用通過調用簡單的接口實現(xiàn)復雜的能力,該層使用C++實現(xiàn),跨平臺。

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

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

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

napi

主要有幾種概念

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

Scope

主要負責Hippy基礎初始化流程,核心步驟如下:

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

注入Natives方法

通過給JS注入Native Function方法的方式,讓JS可以直接調用終端方法;主要是常見的JS側CallNative方法均通過此進行分發(fā)。

執(zhí)行JS Native Source Code

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

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

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

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

Abilities

C++ Module

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

TurboModule

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

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

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

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

Dynamic Import

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

HippyCore異常處理

分兩種異常

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

JS引擎接口報的異常

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

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

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

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

總結

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

Hermes接入

本來是可以很詳細介紹的,但是受限于篇幅不宜過長,后面在開一篇來講。

Hermes接入對比

性能

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

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

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

另外通過線上業(yè)務大盤整體耗時曲線圖可以更直觀看到效果(大部分業(yè)務沒有全量,所以還會有持續(xù)下降的趨勢):

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

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

內存

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

Crash

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

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

JSC Crash關鍵詞:jscctx/HippyJSCExecutor Hermes Crash關鍵詞:hermes/HippyHermesExecutor

計劃的事情

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

剩余重要的事情主要有:

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

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

3. 基于Hermes的內存調試診斷工具;

最后

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

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

相關新聞

聯(lián)系我們

聯(lián)系我們

400-9010-860

在線咨詢:點擊這里給我發(fā)消息

微信:85018612

商夢建站客服

工作時間:周一至周六

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

關注微信
關注微信
分享本頁
返回頂部