返回博客首页
← 所有文章

NativeScript Angular - 性能技巧

2017 年 8 月 22 日 — 作者 Sean Perkins

Sean Perkins 是 Maestro, LLC 的首席研究与设计工程师

概述

  1. 性能挑战
  2. 分析您的应用程序 - iOS
  3. 优化您的视图
  4. 技巧、限制和未来路线图

目标: 

精心设计:正确构建您的应用程序架构可以对导航时间、页面渲染性能和应用程序维护产生重大影响。 

性能挑战

原生应用程序中的 Web 心态行不通

如果您来自 Web 开发背景,您可能会对 NativeScript 提供的众多不同布局类型感到困惑:StackLayout、GridLayout、AbsoluteLayout 等。世界变得比简单的 div 和用于定位的样式复杂得多。 

这就是您的初始倾向可能导致您的应用程序架构出现问题的所在。让我们举一个 UI 实现的例子来详细说明这一点。 

场景:您被要求构建一个简单的下拉菜单元素。下拉菜单不是原生控件,因为您有一些在 iOS 或 Android 选择器中无法实现的样式限制。 

示例设计模拟

Screenshot 2017-08-10 11.04.47 

在阅读了 NativeScript 文档后,您可能会决定 StackLayout 最适合将标签设计与右侧的自定义图标叠加。为了包含外部下拉菜单组件,您当然需要一个容器来设置宽度、高度和边框。假设您模拟了一些类似于此的代码


<StackLayout borderColor="#D6D6E4" borderWidth="1" borderRadius="5" backgroundColor="white">
   <StackLayout orientation="horizontal">
       <Label class="accent" text="Date"></Label>
       <!-- Custom Icon Directive -->
       <Label horizontalAlignment="right" icon="sort"></Label>
   </StackLayout>
   <StackLayout orientation="horizontal">
       <Label text="Alphabetical"></Label>
   </StackLayout>
</StackLayout>

有一个布局用于使用外部样式,一个布局用于包含每一行,等等。这就是开发人员开始走下坡路的地方。当这段代码以原生方式渲染时,您会创建3 个视图容器。在更大的图景中,3 个容器是什么?仅仅对于这个下拉菜单组件来说,我们就在应用程序的渲染上引入了以下复杂性。

VC = N + 1
VC = view containers
N = number of options
假设您对导航、卡片设计、搜索栏以及应用程序中的每个微型组件都遵循这种心态……您最终会给您的原生应用程序带来巨大的渲染负担。

NativeScript 是单线程的:您知道所有 UI 和交互都在主 UI 线程上运行吗?这意味着您会为应用程序渲染的每个容器增加负担。容器数量还会影响底层原生框架及其渲染和布局时间。

注意NativeScript 允许您使用后台工作线程,但这些工作线程并不适用于 UI,而是用于诸如 http 调用、大型数据操作等服务。 

我们如何克服这一挑战?我们将在接下来的几节中介绍,但首先让我们分析一下您当前的应用程序,看看您是否陷入了这种方法的陷阱。 

分析您的应用程序 - iOS

先决条件:您需要在机器上安装 Xcode。

通过命令行或直接通过 Xcode 运行您的应用程序。如果您通过命令行运行应用程序,您需要将 Xcode 调试器附加到您的应用程序。应用程序启动并渲染到您的模拟器上后,导航到特定页面以获取快照。 

选择您的应用程序,然后转到“调试导航器”。

Screenshot 2017-08-10 11.04.58

在屏幕的底部中央,您需要选择“调试视图层次结构”(图标看起来像 3 个方块)。

Screenshot 2017-08-10 11.05.04

Xcode 将获取快照,并允许您查看原生应用程序上容器的层次结构。在我们的示例应用程序(已经过相当程度的优化)中,我们有以下快照

Screenshot 2017-08-10 11.05.13

请注意,每次您导航到或从路由导航时,您的应用程序都必须重新渲染其中许多视图。如果您在设备上注意到导航滞后,则很可能是由于视图过多造成的。 

您的应用程序需要多少个视图才算合适?这取决于情况。仅在 iOS 设备中,分配的 RAM 量从 Air 1 到 Air 2 各不相同。像 iPad Pro 这样的新设备可以渲染优化不佳的应用程序,尽管这需要更高的 CPU 使用率和内存消耗。 

作为最佳实践,您应该尝试按照以下两个原则来完成您的 UI

  1. 尽可能使用原生 UI(即使移植到 NativeScript 的插件也可能使用更简洁的视图布局)。
  2. 尽可能减少视图数量以完成您的 UI。这意味着要跳出文档和当前可用示例的局限性思考。

优化您的视图

使用我们的种子项目,我们共享用于完成 Web 和原生应用程序的代码。我们有几十个共享组件;从下拉菜单到卡片布局、按钮控件、导航栏等等。 

属性选择器与元素选择器

使用 Angular CLI,我们的组件以元素语法生成。这是您可以利用的第一个优化,它可以提高 Web 和原生性能。组件装饰器允许您指定选择器

@Component({
   moduleId: module.id,
   selector: 'dropdown, [dropdown]', // notice [dropdown]!
   templateUrl: './dropdown.component.html',
   styleUrls: ['./dropdown.component.scss']
})

现在,您的组件可以附加到父容器,简化微型组件的复杂性。 

优化前


<GridLayout>
   <StackLayout>
       <dropdown></dropdown>
   </StackLayout>
</GridLayout>

优化后


<GridLayout>
   <StackLayout borderWidth=”1” borderColor=”#D6D6E4” dropdown></StackLayout>
</GridLayout>
现在,我们可以从微型组件中删除 StackLayout,并将样式属性附加到父容器;这样,就不必渲染外部 UIView/StackLayout。 


但是等等 - 我们不能直接将 StackLayout 不放入父视图中吗……这有什么用?

示例视图已简化,并假设其他 UI 将位于下拉菜单的上面或下面。在之前的版本中,我们对微型组件没有任何样式控制,即它在父视图中渲染时的方式。请考虑更复杂的视图


<GridLayout rows="160, 40, *">
   <banner></banner>
   <StackLayout row="1">
       <dropdown></dropdown>
   </StackLayout>
   <StackLayout row="2"
       <card-table></card-table>
   </StackLayout>
</GridLayout>


为了实现删除这两个 StackLayout 复杂性,我们必须在微型组件上添加输入以接受属性;这样,微型组件就可以智能地处理它将渲染在 GridLayout 的哪一行。默认情况下,您的自定义组件不知道如何处理原生布局的样式属性,并在第 0 行第 0 列渲染。

属性不仅是一种更简洁的方法,而且也是开发人员了解您的组件在做什么的更易读的技术。 

旁注:在我们的应用程序中,我们的团队利用了属性选择器和绑定到 NativeScript 属性这两种技术,因为有些视图过于复杂或可重用,无法通过其他方式处理。 

GridLayout 是王道

GridLayout 结合了所有优点。它允许您在不同的行、列和不同大小的属性上渲染 UI。在许多情况下,您实际上可以完全跳过使用 StackLayout。回到我们的下拉菜单示例,我们可以使用单个视图容器和一些ng-template 的技巧完全重新创建视图。 

完整示例


<GridLayout verticalAlignment="top"
   rows="auto"
   [width]="width"
   padding="20"
   horizontalAlignment="right"
   borderColor="#D6D6E4"
   borderWidth="1"
   borderRadius="5"
   backgroundColor="white"
   [margin]="margin">
   <ng-template ngFor let-option [ngForOf]="options" let-i="index">
       <Label [class.accent]="option.selected" (tap)="toggleSort($event, option)"
           [row]="i"
           [marginTop]="i * 35"
           class="semi-bold"
           verticalAlignment="top"
           [text]="option.label | translate"></Label>
       <Label *ngIf="option.selected"
           [row]="i"
           [marginTop]="i * 35"
           icon="sort"
           verticalAlignment="top"
           horizontalAlignment="right"
           class="accent"
           (tap)="toggleSort($event, option)"
           [rotate]="sortDir === 'desc' ? 180 : 0"></Label>
   </ng-template>
</GridLayout>

这些内容很多,所以让我们列举几个我们正在做的事情。
  1. 利用 GridLayout 作为外部容器,使我们的行可以自动调整到其内容的大小。
  2. 使用ng-template 来避免另一个视图容器,并获取“选项”重复器的索引。
  3. 动态绑定每个 Label 的,使其为其索引位置。
  4. 使用marginTop 计算来“伪造”堆叠,将 Label 向下推。

我们的视图容器复杂性方程现在是


VC = 1
VC = view container


这意味着,无论选项数量多少,我们都不会再有额外的视图容器需要渲染。您确实需要渲染 Label,但这只是一点点性能损耗,您已经必须考虑到这一点。 

最佳实践、限制和未来路线图

最佳实践

在我们自己的搜索中,我们发现了一些有用的技巧、窍门和要求,以充分利用 NativeScript Angular 应用程序,从而获得更好的性能。

  1. 您必须使用 Webpack、AOT 和 Uglify
  2. 尽可能延迟加载模块
  3. *ngIf 包装复杂的视图,以延迟渲染执行
  4. 在后台预加载延迟加载的模块
  5. 减少视图复杂性

Webpack、AOT 和 Uglify

如果您不熟悉 webpack、AOT 和 uglify 的性能优势;那么您首先会注意到应用程序包的大小减小。在我们的应用程序中,我们从 50+mb 降至大约 35mb。 

除了大小减小之外,您的应用程序还会更快地启动 - 消除您可能遇到的许多“白屏”效果。 

延迟加载的模块

通过将模块分解,它们将被构建成单独的“块”。这意味着您的应用程序只会根据实际需要请求该块。这样可以避免许多获取应用程序所需信息之外的信息的开销。 

包装复杂的视图

对于这种技术,我需要感谢 Eddy 在这个 git 问题中提出的想法。通过延迟复杂视图的渲染,它允许您的导航事件在主线程上尽快完成,让用户感觉应用程序没有滞后。您可以根据视图的密集程度调整超时的阈值。

组件类


renderView = false;
renderViewTimeout: any;

ngAfterContentInit() {
   this.renderViewTimeout = setTimeout(() => {
       this.renderView = true;
   }, 300);
}
ngOnDestroy() {
   clearTimeout(this.renderViewTimeout);
}

预加载延迟加载的模块

同样感谢 Eddy,这种修改允许您的应用程序在后台获取注册到特定路由模块的所有延迟加载的模块。在我们自己的应用程序中,我们没有注意到性能有显著提升,但还是值得一提。 

应用程序路由模块(app-routing.module.ts


NativeScriptRouterModule.forRoot(
   <any>APP_ROUTES, { preloadingStrategy: PreloadAllModules })

减少视图复杂性

有些设计对于原生应用程序来说没有意义,尤其是对于 NativeScript 应用程序而言。NativeScript 以其能够访问原生 API 而闻名 - 而不是渲染复杂的 UI。将大型视图移动到使用分段视图或选项卡视图,并拒绝容器繁重的设计。

如果您是一位经验丰富的原生开发人员,或者可以访问一位原生开发人员,您也可以原生创建组件,然后将它们移植到 NativeScript。 

限制

一个空白的 NativeScript Angular 项目启动时的内存消耗约为 100mb。虽然 NativeScript 的每次发布周期都比上一次发布带来了巨大的改进,但如果能够在新的项目中看到更低的开销,那就太好了。

作为经验法则,您的应用程序的内存消耗不应超过设备分配内存的 45%。从一开始,NativeScript 就会消耗 100mb,这意味着对于比 Air 2 更老的设备,您只有 360.8 mb 可以使用。这意味着您的内存分配的 20% 只是因为您使用了带有 JavaScript Core 引擎的 NativeScript。 

您可以进行一些修改,例如使用 WebPack & AOT 将其降低到大约 90mb,但这似乎仍然很高,因为只是渲染了一个空白视图。 

未来路线图

在撰写本文时,NativeScript 将在本月底发布 3.2 版本。此版本包括视图回收,以及对 tns-ios 3.0 中引入的内存泄漏的修补程序。这两项更改都为您的应用程序带来更好的性能铺平了道路。 

祝您好运,欢迎在下方评论或通过 NativeScript 论坛、Slack 社区或 Github 联系我。