性能一直是 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
控制台日志 时间轴:运行时:公开: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 性能改进的一个很好的例子。以下是细分结果
以下是通过更新 PracticeBuddy 的 app/package.json 文件来启用跟踪并使用以下命令运行应用程序获得的数字
tns run android | timeline-view
所有这些加起来总共大约 9 秒。
原生 tns run 不包含 webpack 和 Angular 工具。要查看添加这些优化步骤带来的差异,让我们使用 webpack 再次运行应用程序
npm run start-android-bundle --uglify | timeline-view
Android 捆绑包和 uglify HTML 时间报告.
所有这些加起来总共大约 3 秒。
请注意,NativeScript 应用程序的默认 webpack 配置将应用程序分为两个主要块 - vendor 和 bundle。Vendor 应该包含来自 node_modules 的所有模块,而 bundle 应该包含来自应用程序文件夹的所有模块。
为了继续进行这些优化,接下来让我们添加 全新的本地 V8 快照 Android 功能
npm run start-android-bundle --uglify --snapshot | timeline-view
以下是该运行的数字
Android 捆绑包、uglify 和快照 HTML 时间报告.
时间进一步缩短
这次,总共大约 2.5 秒。
分析中的“提取资产”部分是读取 JavaScript、XML 和 CSS 文件所需的时间。但是,为什么我们在 webpack 可以将它们全部打包成 JavaScript 字符串,并且它们实际上可以进入快照时需要资产提取呢?这些资产仅在应用程序首次启动时提取一次。后续启动将不包括该时间。因此,这可能是我们在未来考虑更好地处理的任务,但在目前,这是一个优先级较低的任务。
这是我们路线图中将在不久的将来解决的“坏家伙”。基本上,当您创建一个函数并将一些对象封装在 JavaScript 中,并将此作为 Java 接口的实现传递时,NativeScript 框架必须在 JavaScript 中的函数处于活动状态时使 Java 对象保持活动状态。这发生在垃圾回收时,NativeScript 将沿着 JavaScript 函数的对象图向上遍历并引用潜在的 Java 对象,以便它们在 Java 垃圾回收中幸存下来。在原生 NativeScript 应用程序或 Android 运行时单元测试中,这还没有成为问题,因为那里的对象图相对较小。现在,使用快照,我们尝试将所有 JavaScript 预先放在 vendor.js 中,并在尽可能早的时间内提供堆,从而使堆变大并对 MarkReachableObjects 产生负面影响。
Ivan Buhov 有一篇关于 快照是什么以及如何启用它们 的广泛博文。
快照的作用是在您的 Mac 上运行一个命令行工具。(快照生成功能目前仅限于 macOS 和 Linux,有关详细信息,请参见上面的链接。)该工具在 V8 实例中执行来自 vendor.js 的所有 JavaScript,然后将内存中的对象捕获到一个大型(会增加应用程序大小)的 blob 中。当应用程序启动时,应用程序只需将 blob 加载到内存中,而不是解析和执行您的 JavaScript,然后继续执行。
每次应用程序在设备上启动时,它都感觉像是一个游戏玩家正在恢复已保存的游戏——应用程序不必重新播放加载、解析和执行 JS。
您可以做两件事来最大限度地提高应用程序的启动性能。
运行 timeline-view 显示了缓慢的 require 路径。您会惊讶地发现,您的平均 NativeScript 应用程序中可能存在多少未使用的 JavaScript。以下是一些我们发现的例子。
为了进一步优化练习伙伴 Android 版本的启动时间,我们在该应用程序的 vendor.ts 文件中添加了以下行
require("rxjs");
require("nativescript-angular/animations");
这在启动期间又节省了 200 毫秒,因为这些文件现在已包含在 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。
以下是在运行原生 NativeScript iOS 构建并将数据管道传输到 timeline-view 工具时获得的统计数据。
tns run ios | timeline-view
这将应用程序加载速度提高了大约 3 秒。
为什么只有 3 秒?上面的统计数据中的前 900 毫秒是调试专用步骤。等待调试时间在发布版本中被移除,实时同步在发布版本中也不可用。因此,发布版本将花费近 3 秒。
接下来,让我们添加 webpack 以及 uglify 压缩,以便从该 Angular 应用程序中获得最佳性能。
npm run start-ios-bundle --uglify | timeline-view
如果减去仅用于调试的“等待调试器”步骤,总共不到一秒钟。
打包将所有文件合并成一个文件,减少文件 I/O,而 uglify 的影响更大,因为 iOS 上的 JavaScriptCore 不会执行 JIT 编译。包提取再次发生,但速度很快,在之前的情况下,由于文件数量多,速度很慢。
图标必须平滑地转换成启动画面,启动画面也必须平滑地转换成您的应用内容。在设计资产时请牢记这一点。
对于 iOS,最好的例子是计算器应用的启动。它完美地遵循了 Apple 的 启动画面的用户界面指南.
对于 Android,启动动画不如 iOS 的流畅,而且启动时间是 iOS 的两倍。但只要遵循这些规则,您仍然会受益。
未样式化内容的闪现是指您的应用在最终“绘制”之前,短暂地以未样式化或不完整的形式出现。这种行为在网页上更常见,因为您需要等待网络上的资源,但在 NativeScript 应用中,如果您的启动过程涉及一些异步任务,您也可能会遇到这种情况。
当我们刚开始测试 PocketBuddy 时,应用在没有操作栏的情况下启动,并以白色绘制状态栏。问题出在哪?好吧,在 Angular 生成主页视图后,操作栏被生成并设置样式,状态栏被渲染为蓝色。因此,看起来状态栏在闪烁。为了解决这个问题,我们在应用的 main.ts 文件中更改了一些代码,以便操作栏最初
platformNativeScriptDynamic({ startPageActionBarHidden: false }).bootstrapModule(AppModule);
<
activity
android:windowSoftInputMode
=
"stateHidden"
... />
在发布之前从 app/package.json 中删除 "profiling": "timeline" 。在发布模式下构建时,iOS 和 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 图标到启动画面的增长过渡和启动画面到应用的淡出过渡会让体验更加出色,你会怎么想?尤其是当原生框架使用引用计数而不是 GC 时。
npm run start-ios-bundle --uglify --release
我不确定如何在发布版本中精确地测量时间,但看起来是这样
该工具使用了一些 NativeScript 内部知识,在某些情况下可能过于冗长,但在其他情况下则存在差距。我们将尝试完善这些手动放置的跟踪的位置。如果这变得足够有用,我们可能会尝试将数据与 Web Inspector 和 Chrome DevTools 中的 timeline 分析工具合并。