返回博客首页
← 所有帖子

深入探讨 NativeScript 3.1 性能改进

2017 年 7 月 20 日 — 作者 Panayot Cankov

性能一直是 NativeScript 在整个框架发展过程中的一项高优先级功能。在开发过程中,我们开发了工具来检测 NativeScript 使用的所有 Objective-C、Java、C、C++ 和 JavaScript 代码,但到目前为止,这些配置文件仅供内部使用,从未发布到 NativeScript 的分发版本中。

在 3.1 中,我们合并了手动检测工具,并使所有 {N} 应用程序能够跟踪一些关键组件的执行时间。本文将向您介绍如何启用此检测,以及如何使用该工具构建启动速度快的 NativeScript 应用程序。

启用跟踪

在您应用程序的 app/package.json 文件中,添加一个新的 profiling 属性,并将其值设置为 timeline

{
    "main": "MyApp",
    "profiling": "timeline"
}


这将启用跟踪,并直接在控制台中输出时间。

此功能处于实验阶段,我们将在 NativeScript 的 @next 版本中对其进行微调。因此,如果您想尝试使用此工具进行分析,请考虑 安装 tns-core-modules 和运行时的下一个版本,以便使用最新最好的代码。

tns platform add android@next
npm i tns-core-modules@next

无论您使用的是 3.1 中附带的分析工具,还是 @next 版本中的工具,在运行应用程序时,您都应该看到很多类似于此的日志条目。

控制台日志 时间轴:运行时:公开:UIViewController  (98155132.912ms - 98155218.291ms)
控制台日志 时间轴:运行时:公开:UIView  (98155124.914ms - 98155221.567ms)
控制台日志 时间轴:运行时:公开:UIResponder  (98155124.841ms - 98155234.224ms)
控制台日志 时间轴:运行时:公开:UIScreen  (98155124.591ms - 98155238.539ms)
控制台日志 时间轴:运行时:require:/app/tns_modules/tns-core-modules/utils/utils.js  (98155114.122ms - 98155239.010ms)
控制台日志 时间轴:运行时:require:/app/tns_modules/tns-core-modules/image-source/image-source.js  (98155239.113ms - 98155260.134ms)
控制台日志 时间轴:运行时:require:/app/tns_modules/tns-core-modules/ui/styling/background.js  (98155097.347ms - 98155268.315ms)
控制台日志 时间轴:运行时:require:/app/tns_modules/tns-core-modules/ui/core/properties/properties.js  (98155295.330ms - 98155313.461ms)

跟踪时间是自 1970 年以来的事件开始和结束时间,以毫秒为单位。 您现在可以概述应用程序执行过程中发生的进程,以及它们花费的时间。

工具

在原始状态下,日志几乎无法用肉眼识别。有一个小工具可以收集跟踪并在 HTML 火焰图中对其进行可视化。要安装该工具,请运行以下命令

npm i -g timeline-view

接下来,再次运行您的应用程序并将输出通过 timeline-view 工具进行管道传输

tns run android | timeline-view
tns run ios | timeline-view

 

运行应用程序,执行要测试的操作,然后在终端中单击 Ctrl + C 以中断 CLI 执行。timeline-view 工具将在退出之前收集跟踪并生成报告。它将在终端中打印报告的位置,以便您可以在浏览器中轻松打开它。

这是报告的外观



让我们看看如何使用此工具来帮助提高真实应用程序的性能。

练习伙伴

练习伙伴 是一个 NativeScript + Angular 应用程序,可帮助音乐教师与学生合作。音乐学生使用该应用程序录制练习课程,教师可以收听并提供反馈。感谢 Jen Looper。

该应用程序是时间轴和 3.1 性能改进的一个很好的例子。以下是细分结果

Android

时间轴:tns run android

以下是通过更新 PracticeBuddy 的 app/package.json 文件来启用跟踪并使用以下命令运行应用程序获得的数字

tns run android | timeline-view


Android 原生运行 HTML 时间报告.

  • 提取资产需要 2100 毫秒
  • 初始化运行时需要 2200 毫秒
  • 加载模块需要 1350 毫秒 - tns-code-modules、angular 等
  • MarkReachableObjects (GC) 耗时 750 毫秒
  • 创建 NativeScript 视图耗时约 2000 毫秒

所有这些加起来总共大约 9 秒。

时间轴:npm run start-android-bundle --uglify

原生 tns run 不包含 webpack 和 Angular 工具。要查看添加这些优化步骤带来的差异,让我们使用 webpack 再次运行应用程序

npm run start-android-bundle --uglify | timeline-view


Android 捆绑包和 uglify HTML 时间报告.

  • 提取资产需要 180 毫秒
  • 初始化运行时需要 180 毫秒
  • 执行 vendor.js 文件需要 750 毫秒
  • 执行 bundle.js 文件需要 320 毫秒
  • MarkReachableObjects (GC) 耗时 390 毫秒
  • 创建 NativeScript 视图耗时约 1000 毫秒

所有这些加起来总共大约 3 秒。

请注意,NativeScript 应用程序的默认 webpack 配置将应用程序分为两个主要块 - vendor 和 bundle。Vendor 应该包含来自 node_modules 的所有模块,而 bundle 应该包含来自应用程序文件夹的所有模块。

时间轴:npm run start-android-bundle --uglify --snapshot

为了继续进行这些优化,接下来让我们添加 全新的本地 V8 快照 Android 功能

npm run start-android-bundle --uglify --snapshot | timeline-view

以下是该运行的数字

Android 捆绑包、uglify 和快照 HTML 时间报告.

时间进一步缩短

  • 提取资产需要 230 毫秒
  • 提取资产需要 230 毫秒
  • 运行时初始化需要 100 毫秒
  • 运行启动器 js 需要 280 毫秒
  • 创建 NativeScript 视图耗时约 1100 毫秒
  • MarkReachableObjects (GC) 耗时 480 毫秒

这次,总共大约 2.5 秒。

提取资产?

分析中的“提取资产”部分是读取 JavaScript、XML 和 CSS 文件所需的时间。但是,为什么我们在 webpack 可以将它们全部打包成 JavaScript 字符串,并且它们实际上可以进入快照时需要资产提取呢?这些资产仅在应用程序首次启动时提取一次。后续启动将不包括该时间。因此,这可能是我们在未来考虑更好地处理的任务,但在目前,这是一个优先级较低的任务。

MarkReachableObjects?

这是我们路线图中将在不久的将来解决的“坏家伙”。基本上,当您创建一个函数并将一些对象封装在 JavaScript 中,并将此作为 Java 接口的实现传递时,NativeScript 框架必须在 JavaScript 中的函数处于活动状态时使 Java 对象保持活动状态。这发生在垃圾回收时,NativeScript 将沿着 JavaScript 函数的对象图向上遍历并引用潜在的 Java 对象,以便它们在 Java 垃圾回收中幸存下来。在原生 NativeScript 应用程序或 Android 运行时单元测试中,这还没有成为问题,因为那里的对象图相对较小。现在,使用快照,我们尝试将所有 JavaScript 预先放在 vendor.js 中,并在尽可能早的时间内提供堆,从而使堆变大并对 MarkReachableObjects 产生负面影响。

V8 快照是什么?

Ivan Buhov 有一篇关于 快照是什么以及如何启用它们 的广泛博文。

快照的作用是在您的 Mac 上运行一个命令行工具。(快照生成功能目前仅限于 macOS 和 Linux,有关详细信息,请参见上面的链接。)该工具在 V8 实例中执行来自 vendor.js 的所有 JavaScript,然后将内存中的对象捕获到一个大型(会增加应用程序大小)的 blob 中。当应用程序启动时,应用程序只需将 blob 加载到内存中,而不是解析和执行您的 JavaScript,然后继续执行。

每次应用程序在设备上启动时,它都感觉像是一个游戏玩家正在恢复已保存的游戏——应用程序不必重新播放加载、解析和执行 JS。

那么如何最大限度地利用它的优势呢?

您可以做两件事来最大限度地提高应用程序的启动性能。

  • 首先,最大限度地减少应用程序所需的 JavaScript 数量。
  • 其次,将尽可能多的 JavaScript 移动到 vendor.js 文件中,以便它进入快照。

运行 timeline-view  显示了缓慢的 require 路径。您会惊讶地发现,您的平均 NativeScript 应用程序中可能存在多少未使用的 JavaScript。以下是一些我们发现的例子。

  • Alexziskind1 的 looptidoo 依赖于名为 faker 的库,而 faker 则需要 30 多个小型语言环境文件。(您的应用程序很少需要多个语言环境文件。)
  • nativescript-marketplace-demo 具有一个显示源代码的页面,该页面使用 highlight.js。Highlight JS 支持多种语言(并且应用程序需要这些语言),但应用程序本身仅需要对 XML 和 JS 源进行突出显示。
  • 练习伙伴需要 Rx.js 的根目录,而通常在使用 Rx.js 时,您只需要使用少量运算符。

为了进一步优化练习伙伴 Android 版本的启动时间,我们在该应用程序的 vendor.ts 文件中添加了以下行

require("rxjs");
require("nativescript-angular/animations");


这在启动期间又节省了 200 毫秒,因为这些文件现在已包含在 webpack 捆绑包中,因此也包含在堆快照中。

Webpack 捆绑包分析器

找到应该从 bundle.js 移动到 vendor.js 的块,可以使用 webpack-bundle-analyzer 来轻松完成,这在“使用 Webpack 捆绑代码”NativeScript 文章中有所介绍。

通常,您应该将 node_modules 中的所有内容移动到 vendor 中。这是通过在 vendor.js 中 require 模块来完成的。但是,一些使用原生 Android API 的插件需要进行重构,因为原生 API 在快照生成期间不可用。您可以查看 Stanimira Vlaeva 为 nativescript-plugin-firebase 制作的 PR,因为它提供了一个很好的示例,说明如何将对 Android Java 类别的访问包装在函数中,并延迟其执行直到运行时。合并后,应在使用该插件的任何应用程序的 vendor.ts 文件中添加 require("nativescript-plugin-firebase")

现在,这是 3.0.0 中应用程序的 webpack 打包且 uglify 压缩的 Android 版本,其中包含对主要时间和添加 3.1.0 快照功能后预期发生情况的解释



现在,我们已经彻底分解了 Android 加载过程,让我们将注意力转向 iOS。

iOS

时间轴:tns run ios

以下是在运行原生 NativeScript iOS 构建并将数据管道传输到 timeline-view 工具时获得的统计数据。

tns run ios | timeline-view


iOS 原生运行 HTML 时间报告.

  • 准备进行实时同步需要 400 毫秒
  • 等待调试器需要 500 毫秒
  • require nativescript-angular/platform 耗时 900 毫秒
  • require app.module.js 耗时 850 毫秒
    • require rxjs/Rx.js 耗时 500 毫秒(此时间已包含在 app.module.js 中)
  • 创建视图耗时约 1000 毫秒

这将应用程序加载速度提高了大约 3 秒。

为什么只有 3 秒?上面的统计数据中的前 900 毫秒是调试专用步骤。等待调试时间在发布版本中被移除,实时同步在发布版本中也不可用。因此,发布版本将花费近 3 秒。

时间轴:npm run start-ios-bundle --uglify

接下来,让我们添加 webpack 以及 uglify 压缩,以便从该 Angular 应用程序中获得最佳性能。

npm run start-ios-bundle --uglify | timeline-view

iOS 包和 uglify HTML 时间报告.

  • 等待调试器需要 500 毫秒
  • vendor.js 中耗时 400 毫秒
  • 创建视图耗时约 500 毫秒

如果减去仅用于调试的“等待调试器”步骤,总共不到一秒钟。

打包将所有文件合并成一个文件,减少文件 I/O,而 uglify 的影响更大,因为 iOS 上的 JavaScriptCore 不会执行 JIT 编译。包提取再次发生,但速度很快,在之前的情况下,由于文件数量多,速度很慢。

甜蜜的超赞

速度快,责任重。现在您的应用在 iOS 上启动时间不到一秒,在 Android 上不到两秒,我们该如何才能在用户点击应用图标到看到应用完整内容的过渡过程中留下最佳印象?您无法使用应用的口号或花哨的动画,因为用户没有足够的时间来观看它。

图标和启动画面

图标必须平滑地转换成启动画面,启动画面也必须平滑地转换成您的应用内容。在设计资产时请牢记这一点。

对于 iOS,最好的例子是计算器应用的启动。它完美地遵循了 Apple 的 启动画面的用户界面指南.

对于 Android,启动动画不如 iOS 的流畅,而且启动时间是 iOS 的两倍。但只要遵循这些规则,您仍然会受益。

未样式化内容的闪现

未样式化内容的闪现是指您的应用在最终“绘制”之前,短暂地以未样式化或不完整的形式出现。这种行为在网页上更常见,因为您需要等待网络上的资源,但在 NativeScript 应用中,如果您的启动过程涉及一些异步任务,您也可能会遇到这种情况。

当我们刚开始测试 PocketBuddy 时,应用在没有操作栏的情况下启动,并以白色绘制状态栏。问题出在哪?好吧,在 Angular 生成主页视图后,操作栏被生成并设置样式,状态栏被渲染为蓝色。因此,看起来状态栏在闪烁。为了解决这个问题,我们在应用的 main.ts 文件中更改了一些代码,以便操作栏最初

platformNativeScriptDynamic({ startPageActionBarHidden: false }).bootstrapModule(AppModule);

几乎完美!在 application:didFinishLaunchingWithOptions:viewWillAppear: 中添加配置文件时间将显示 Angular 框架将在这些方法之外实例化原生视图,可能是承诺上的进程链。初始页面将显示没有内容,实际内容将在稍后出现,可能跳过启动画面和应用内视图之间的淡入淡出动画。只需在 app.css 中为页面和操作栏视图提供适当的样式,而不是在主组件的 css 中提供样式,这样延迟就不会闪烁为未样式化的页面。

没有启动键盘

键盘需要时间来绘制并使用动画打开。键盘还会隐藏主页上登录表单内容的一部分。首次使用用户可能没有帐户。他们需要注册,如果键盘隐藏了“注册”按钮,即使“用户名”字段最初处于焦点状态,也是不好的。因此,作为一般经验法则,请避免在应用的第一个屏幕上自动显示软件键盘。为了在 Android 中禁用键盘,您可以通过添加以下内容来配置 app/App_Resources/Android/AndroidManifest.xml
<activity android:windowSoftInputMode="stateHidden" ... />

最终的发布派对

在发布之前从 app/package.json 中删除 "profiling": "timeline" 。在发布模式下构建时,iOS 和 Android 平台都会跳过一些调试内容并加快速度。

Android

npm run start-android-bundle --uglify --snapshot -- --release <keystore options> | timeline-view


后续的热运行将在 Android 上花费不到 2 秒的时间

adb logcat | grep Displayed
com.ladeezfirstmedia.practicebuddy/com.tns.NativeScriptActivity: +1s850ms


iOS

如果我告诉你,内置的 iOS 图标到启动画面的增长过渡和启动画面到应用的淡出过渡会让体验更加出色,你会怎么想?尤其是当原生框架使用引用计数而不是 GC 时。

npm run start-ios-bundle --uglify --release

 

我不确定如何在发布版本中精确地测量时间,但看起来是这样

下一步

该工具使用了一些 NativeScript 内部知识,在某些情况下可能过于冗长,但在其他情况下则存在差距。我们将尝试完善这些手动放置的跟踪的位置。如果这变得足够有用,我们可能会尝试将数据与 Web Inspector 和 Chrome DevTools 中的 timeline 分析工具合并。