关于后端:如何不改一行代码让Hippy启动速度提升50

1次阅读

共计 7300 个字符,预计需要花费 19 分钟才能阅读完成。

导读|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;

// 获取属性值并转化成 object
Object 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;

// 获取属性值并转化成 object
Object 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++ 的缺点与思考(下)

正文完
 0