如何加速你的Angular应用

Posted Angular中文社区

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何加速你的Angular应用相关的知识,希望对你有一定的参考价值。

(这是来自阿里云的VTHINKXIE同学翻译的文章,内容很详实,故转发过来,已经获得VTHINKXIE的同意。里面一些外部资源链接都失效了,如果你想看到那些内容,请点最底部的“阅读原文”。)



Angular一直宣称在默认情况下它的运行的速度很快。在实际情况中,Angular程序运行速度必然与运行环境相关,它会受到程序的用途、在同一时刻程序处理的事务、程序的组织结构和程序中的双向绑定数量的影响。当我们试图让Angular程序运行更快时,我们就可能会面临到这些影响程序性能的问题。


几周之前,作者在 NG-BE 上和Oliver Zeigermann做了 Angular and React - Friends learning from each other的演讲,在这个演讲上,两人讨论了Demo程序的默认性能和使其运行更快的方法。


在本文中我们将使用这个Demo来介绍一些加速应用的技巧,而这些技巧对于你的Angular程序应该也会有用。


本文作者在另一篇文章中介绍了另外一种加快Angular应用的方法:Zones in Angular


渲染10000个可拖拽的SVG box


我们想找出一个可以达到框架性能瓶颈的场景,这样性能的提升可以更加容易的被感知到。这个场景不必是真实的应用场景,但是需要有足够的挑战难度。这就是为什么我们选择10000个可拖拽的SVG box的原因。渲染10000个SVG box并不是很难的工作,但是当每个Box都可被拖拽的时候,事情就变得有趣很多。在Angular框架中,mousemove事件被触发的同时会触发Angular的脏值检查策略,当10000个box在一起时,对框架而言这是相当繁重的工作。


读者可以通过这篇文章进一步了解Angular的脏值检查策略

这个程序包含了两个Component,分别是AppComponent和BoxComponent,AppComponent的代码如下


@Component({
  selector: 'demo-app',
  template: `
    <svg (mousedown)="mouseDown($event)"
         (mouseup)="mouseUp($event)"
         (mousemove)="mouseMove($event)">

      <svg:g box *ngFor="let box of boxes" [box]="box">
      </svg:g>

    </svg>
  `})export class AppComponent implements OnInit {

  currentId = null;
  boxes = [];
  offsetX;
  offsetY;

  ngOnInit() {
    this.boxes = ... // generate 10000 random box coordinates
  }

  mouseDown(event) {
    const id = Number(event.target.getAttribute("dataId"));
    const box = this.boxes[id];
    this.offsetX = box.x - event.clientX;
    this.offsetY = box.y - event.clientY;
    this.currentId = id;
  }

  mouseMove(event) {
    event.preventDefault();
    if (this.currentId !== null) {
      this.updateBox(this.currentId, event.clientX + this.offsetX, event.clientY + this.offsetY);
    }
  }

  mouseUp() {
    this.currentId = null;
  }

  updateBox(id, x, y) {
    const box = this.boxes[id];
    box.x = x;
    box.y = y;
  }}

上面代码中需要注意的是

  • 我们在SVG元素增加了mousedown、mousemove和mouseup的事件监听

  • 我们使用ngFor生成了10000个随机位置的box

  • 当拖拽发生时我们通过mouseMove修改box的位置


接下来我们来看BoxComponent的代码

@Component({
  selector: '[box]',
  template: `
    <svg:rect
      [attr.dataId]="box.id"
      [attr.x]="box.x"
      [attr.y]="box.y"
      width="20"
      height="20"
      stroke="black"
      [attr.fill]="selected ? 'red' : 'transparent'"
      strokeWidth="1"></svg:rect>
  `})export class BoxComponent {
  @Input() box;
  @Input() selected;}

BoxComponent只是渲染了一个SVG rect元素,并在其上绑定了多个数据用于确定其坐标。


我们的程序就这样完成了,在以下链接可以查看程序实际的运行情况

https://plnkr.co/edit/UBI5Sc5eDMpkDDkJfeGX?p=info


当点击和拖拽Box时,我们能感觉到整个程序非常的janky,是时候去监测该程序的运行性能了。


请注意,运行在Plnkr中的Angular程序均运行在JiT模式,并不能代表实际的运行性能,建议在本地进行性能测试


监测程序性能


我们关注程序的runtime performance和mousemove触发时Angular处理任务耗时,这也是在拖拽box时Angular实际要完成的工作。


Chrome开发工具中的Performance选项卡可以让我们很容易的对程序的性能进行监测,按照以下步骤进行操作

  • 打开Chrome开发工具(ALT + CMD +I)

  • 选择Performance选项卡

  • 点击左上角的Record按钮(CMD + E)

  • 选中并拖拽box

  • 再次点击Record按钮,停止记录


理想情况下,任何类型的测量都应该在类似Chrome的隐身窗口中进行,这样记录的数字不会受浏览器插件或其他浏览器Tab使用的资源的影响。 此外,每次测量结果可能会稍有改变,试着进行3-5次测量后取平均值。


以下是在 MacBook Air (1,7 GHz Intel Core i7, 8 GB DDR3) 使用Chrome (Version 55.0.2883.95 (64-bit))的测量结果


  • 1st Profile, Event (mousemove): ~40ms, ~52ms (最快, 最慢)

  • 2nd Profile, Event (mousemove): ~45ms, ~61ms (最快, 最慢)

  • 3rd Profile, Event (mousemove): ~41ms, ~52ms (最快, 最慢)


请注意到每次移动鼠标的时候会触发多次mousemove事件,所以我们需要记录最快和最慢的数值,当然,这些数值在你本机上可能会不同。Angular平均耗时42ms~55ms来渲染10000个box,考虑到完全没有进行优化,这个性能不算太糟。但是我们想要让它运行的更快,让我们继续。


怎样运行的(更)快


开箱即用的Angular已经运行的很快,然而事实证明如果想要运行的更快,我们还有很多事情可以做。以下是我们与Angular团队的核心成员Tobias合作提出的性能改进方案,Tobias在Angular团队中主要负责compiler部分,他显然知道如何让Angular程序运行更快。


  • ChangeDetectionStrategy.OnPush - 使用Angular的OnPush脏值检查策略来减少每个脏值检查任务中的需要被对比的绑定值

  • 自定义简单版本NgFor - Angular的NgFor过于smart,改用自定义的简单版本NgFor能够提升这部分的性能。

  • Detach Change Detectors - 另外一个选择就是从Component上detach掉所有的脏值检查探测器,只有在Component中发生数据改变时手动调用脏值检查策略。


使用OnPush脏值检查策略


使用OnPush脏值检查策略可能是最显然的选择,在 Change Detection in Angular 文章中,我们讨论了Angular了OnPush脏值检查策略可以使Angular在数据发生改变时减少脏值检查的次数。


在使用OnPush脏值检查策略后,Angular只会在Component的@Inputs的数值发生改变(不是mutate)后才会对视图绑定数据进行脏值检查,在Demo中我们只有AppComponent和BoxComponent两个Component,并且只有BoxComponent接收Inputs输入。如果我们将BoxComponent的脏值检查策略修改为OnPush,在每个box上我们能减少4次绑定检查(因为box视图上绑定了4个属性)。当一个Box发生改变时可以节省39996次绑定检查。为了使用OnPush,我们需要在我们的代码中做两处很小的修改:修改ChangeDetectionStrategy和确定BoxComponent的输入Immutable。


修改ChangeDetectionStrategy:

import { 
  Component,
  Input,
  ChangeDetectionStrategy} from '@angular/core';@Component({
  selector: '[box]',
  changeDetection: ChangeDetectionStrategy.OnPush, // set to OnPush
  ...})export class BoxComponent {
  @Input() box;
  @Input() selected;}


为了确保所有的Inputs Immutable,每次updateBox时生成新的Object


@Component(...)class AppComponent {
  ...
  updateBox(id, x, y) {
    this.boxes[id] = { id, x, y }; // new references instead of mutation
  }}


这样就完成了,以下是已经性能优化后的Demo

https://plnkr.co/edit/F9DgvCHV7ayw4x9545DM


请注意,运行在Plnkr中的Angular程序均运行在JiT模式,并不能代表实际的运行性能,建议在本地进行性能测试


如何加速你的Angular应用


此时你可能很难注意到实际的区别,因为之前未优化的Demo已经运行的很快了,我们再次对程序性能进行监测来查看优化后的程序是否真正运行的更快了。


  • 1st Profile, Event (mousemove): ~25ms, ~35ms (最快, 最慢)

  • 2nd Profile, Event (mousemove): ~21ms, ~44ms (最快, 最慢)

  • 3rd Profile, Event (mousemove): ~23ms, ~37ms (最快, 最慢)


我们可以看到,OnPush显然提升了程序的运行性能。我们也许没有感受到实际的视觉影响,但是我们可以在性能数据上观察到结果,我们的程序运行的速度是之前的两倍。考虑到只有很少的改变就可以得到这样的优化,这是一个很多好的结果。


实际上,我们可以进一步优化我们的程序。现在10000个box还是会进行脏值检查,只有视图层的脏值检查被跳过了(39996个绑定)。这是因为为了要找到一个Component的Input是否改变,Angular需要检查每一个Component。我们可以使用某种分隔模型将10000个box分隔开,比如10个部分,每部分有1000个box,这会将需要被检测的Component数量降低到999个。


但是这种做法只会让我们的代码变得更加复杂,同时还改变了整个程序的算法,我们应当避免这些事情发生。


使用自定义简化版NgFor

另外一个在Demo中不太容易被注意到的地方是NgFor可能会产生不必要的性能开销。让我们看下NgFor的实现代码,我们可以发现NgFor指令不仅遍历创建了DOM实例,还对每个DOM实例的位置保持了追踪。这个特性对于animation动画非常有用,我们可以通过这个特性很方便的创建实例动画。


然而在我们的Demo中,我们并没有真正移动NgFor创建的实例本身,甚至根本没有触及它。所以为什么我们不使用一个更简单不关心位置的NgFor呢?创建这样一个指令需要一些工作量,关于SimpleNgFor的实现和原理超出了本文要讨论的范围,我们将在另外一篇文章中介绍它。SimpleNgFor的源代码可以在以下链接中找到


https://plnkr.co/edit/rCqTphznFbImsy9wbWP6


SimpleNgFor (不使用 OnPush)

  • 1st Profile, Event (mousemove): ~45ms, ~50ms (最快, 最慢)

  • 2nd Profile, Event (mousemove): ~43ms, ~53ms (最快, 最慢)

  • 3rd Profile, Event (mousemove): ~42ms, ~50ms (最快, 最慢)

SimpleNgFor (使用 OnPush)

  • 1st Profile, Event (mousemove): ~22ms, ~32ms (最快, 最慢)

  • 2nd Profile, Event (mousemove): ~22ms, ~39ms (最快, 最慢)

  • 3rd Profile, Event (mousemove): ~21ms, ~30ms (最快, 最慢)


我们进行了6次性能监测,其中3次使用这个Demo,另外三次还额外使用了OnPush的脏值检查策略。我们可以看到SimpleNgFor提升了一点点性能。考虑到开发SimpleNgFor指令的工作量,平均只降低了5ms的时间可能并不划算。


Detach脏值检查探测器


Angular可以让开发者很好的控制在框架中事物的处理过程。 OnPush使开发者能够决定当数据发生改变时在Component树中何时何地跳过脏值检查。 虽然这已经很强大,但在有限现实情况下还不够。 这就是为什么Angular让我们可以访问每个组件的脏值检查探测器ChangeDetectorRef,我们可以通过它来完全的启用或禁用脏值检查。 在这篇文章中我们也谈到了这一点,接下来我们来讨论Detach脏值检查探测器在我们的Demo中该如何运用。


在每次改变中(mousemove 事件发生时)10000个可被拖拽的SVG box每个都要被检查一次,在之前我们已经讨论过,在我们现在的程序架构中,OnPush不会阻止Angular去检查box本身,只会阻止检查box的视图绑定数据。然而如果我们将所有的Component的脏值检查完全关闭,只当box component被真正移动的时候再去进行脏值检查呢?这样明显会减少每个任务的工作量,因为我们只用对一个box进行脏值检查,而不再是10000个。


想要实现以上的构想,我们首先要做的事情是将Component的脏值检查探测器从Component中detach,我们依赖注入ChangeDetectorRef,使用它的detach()方法。唯一需要我们注意的是我们需要在第一次脏值检查后再detach,否则我们不会看到任何box。为了能够在正确的时机调用detach(),我们可以使用Angular的AfterViewInit钩子函数。


代码看起来是这个样子

import { AfterViewInit, ChangeDetectorRef } from '@angular/core';@Component(...)class AppComponent implements AfterViewInit {
  ...
  constructor(private cdr: ChangeDetectorRef) {}

  ngAfterViewInit() {
    this.cdr.detach();
  }}


对于所有的box component做法也一样


@Component(...)class BoxComponent implements AfterViewInit {
  ...
  constructor(private cdr: ChangeDetectorRef) {}

  ngAfterViewInit() {
    this.cdr.detach();
  }}


现在我们可以看到所有的box,但是拖拽不再起作用了。这种情况是正常的,因为脏值检查已经被全部关闭,所有的事件自然也不再会起作用了。


接下来我们要做的事情是确保当box被拖拽时,脏值检查策略会被调用。我们给BoxComponent增加update()方法用于执行脏值检查


@Component(...)class BoxComponent implements AfterViewInit {
  ...
  update() {
    this.cdr.detectChanges();
  }}


现在我们有了在mouseDown(),mouseMove()和mouseUp()中我们需要调用的方法,但是我们怎么才能获得被拖拽的box component呢?依赖this.boxes[id]不会再起作用,因为它不是BoxComponent的实例。我们需要拓展event的对象以便我们能够获得该实例。


这或许看起来有一些hacky,我们期待BoxComponent的实例出现在DOM event对象中,以便我们能够从类似mousedown这种事件中像这样获取到:


@Component(...)export class AppComponent implements AfterViewInit {
  ...
  mouseDown(event) {
    const boxComponent = <BoxComponent> event.target['BoxComponent'];

    const box = boxComponent.box;
    const mouseX = event.clientX;
    const mouseY = event.clientY;
    this.offsetX = box.x - mouseX;
    this.offsetY = box.y - mouseY;

    this.currentBox = boxComponent;

    boxComponent.selected = true;
    boxComponent.update();
  }}


为了让box component的实例能出现在event.target对象中,在这里target是SVG rect元素,我们可以通过BoxComponent的ViewChild获取到它,并将BoxComponent的实例作为它的新属性。我们给template中添加了#rect的标记以便我们能够通过ViewChild('rect')获取到它。


现在我们可以使用BoxComponent.update()方法来实现我们想要的效果了。

完整的Demo代码可以在以下链接中看到,在这个Demo中只有被拖拽的Box会进行脏值检查。

https://plnkr.co/edit/3aVnNf7sogtvUa9Nng51


javascript的执行时间降低到了1ms以下,Angular只对用户拖拽的box进行了脏值检查。这个例子非常好的显示了Angular给予开发者足够的控制权用于优化特定需求的程序。


总结


Angular在默认情况下运行很快,但它仍然提供控制脏值检测的工具用于进一步优化应用性能。


所有的Demo都运行在Angular的dev模式下,我们可以通过打开production模式来进一步提升性能。在production模式下所有的脏值检查只会运行一次,而在dev模式下会运行两次。


我们应该记住,没有任何一个技巧是能够解决所有问题的银弹(silver bullet),它们可能适用也可能完全不适用于你开发时的特定场景。

希望通过本文你已经了解到如何让Angular程序运行更快的方法。


以上是关于如何加速你的Angular应用的主要内容,如果未能解决你的问题,请参考以下文章

第1270期老树发新芽—使用 mobx 加速你的 AngularJS 应用

Spark:如何加速 foreachRDD?

如何在Angular2 rc3路由中处理来自oauth重定向url的哈希片段

如何使用 Swift 使用此代码片段为 iOS 应用程序初始化 SDK?

11.按要求编写Java应用程序。 创建一个叫做机动车的类: 属性:车牌号(String),车速(int),载重量(double) 功能:加速(车速自增)减速(车速自减)修改车牌号,查询车的(代码片段

按要求编写Java应用程序。 创建一个叫做机动车的类: 属性:车牌号(String),车速(int),载重量(double) 功能:加速(车速自增)减速(车速自减)修改车牌号,查询车的载重量(代码片段