AngularJS Performance in Large Applications翻译

原文:https://www.airpair.com/angularjs/posts/angularjs-performance-large-applications#5-1-scopes-and-the-digest-cycle

1介绍

无论您是为大型的旧应用程序编写Angular,还是您已有的Angular应用程序在变得庞大,性能是一个重要的方面。 了解AngularJS应用程序减慢的原因以及如何在开发过程中做出权衡很重要。 本文将介绍AngularJS的一些常见的性能问题,以及如何避免和修复的建议。

1.1要求,假设

本文将会假定你对JavaScript语言和AngularJS有些熟悉。 当使用特定版本的功能时,它们将被调用。 为了充分利用这篇文章,最好是用过一段时间Angular,但还没有认真对待性能。

2衡量工具

2.1基准测试

jsPerf是一个很棒的用于对代码进行基准测试的工具。 我将在相关部分结尾给出具体的测试链接,以便阅读。

2.2 分析

Chrome开发工具有一个很好的JavaScript分析器。 我强烈推荐阅读本系列 文章。

2.3 Angular Batarang

Angular Batarang是一个专注于angular的调试器,由Angular Core Team维护,可在GitHub上获得。

3软件性能

决定是否高性能软件有两个根本原因。
第一个是算法时间复杂度。 解决这个问题很大程度上超出了本文的讨论范围,一般来说,时间复杂度是程序需要做多少计算来取得结果的一个衡量标准。 计算的数量越大,程序越慢。 一个简单的例子是线性搜索与二进制搜索。 线性搜索需要对同一组数据进行更多计算,因此将会更慢。 有关时间复杂性的详细讨论,请参阅维基百科文章
第二个原因就是算法空间复杂度。 这是计算机运行算法需要多少“存储空间”或内存的衡量标准。 需要的内存越多,解决方案越慢。 本文讨论的大多数问题在空间复杂性之下将会变得松动。 详细讨论请看这里
这句话不知道怎么翻译,有知道的朋友请告知,谢谢。原文:

Most of the problems this article will talk to fall loosely under space complexity. For a detailed discussion, see here.

4 Javascript 性能

这里说下关于JavaScript 性能的几件事情,不一定局限于angular。

4.1 循环

避免在循环中调用。 如果循环内的调用可以在循环之外执行,那么把它放到循环之外将极大地加快您的系统。 例如:

var sum = 0;
for(var x = 0; x < 100; x++){ 
    var keys = Object.keys(obj); sum = sum + keys[x];
}

会显着慢于:

var sum = 0;
var keys = Object.keys(obj);
for(var x = 0; x < 100; x++){ 
    sum = sum + keys[x];
}

http://jsperf.com/for-loop-perf-demo-basic

4.2 DOM访问

注意DOM访问,这很重要。

angular.element( 'div.elementClass')

代价昂贵。虽然在AngularJS中出现这种问题的几率很小,但还是有必要了解这一点。这里要说的第二件事就是在可能的情况下,DOM树应该保持较小。
最后,尽可能避免修改DOM和设置内联样式。因为这会导致JavaScript重绘。重绘的深入讨论超出了本文的范围,但这里有一个很棒的参考

4.3变量作用域和垃圾回收

尽可能严格地将所有变量声明为局部作用域,以使JavaScript垃圾回收器能够更快地释放内存。
这句我是参照上下文推断出来的,翻译的可能有点问题。可看原文:

Scope all variables as tightly as possible to allow the JavaScript garbage collector to free up your memory sooner rather then later.

这是造成JavaScript,特别是Angular缓慢,滞后,不响应非常常见的原因。请注意以下问题:

function demo(){
    var b = {childFunction:function(){console.log('hi this is the child function')};
    b.childFunction();
    return b;
  }

当函数终止时,将不再有对b可用的引用,垃圾回收器将释放内存。但是,如果在其他地方有这样的一行:

var cFunc = demo();

我们现在将对象绑定到一个变量并保持对它的引用,从而防止垃圾收集器清理它。虽然这可能是必要的,但重要的是要注意这对对象引用的影响。

4.4数组和对象

这里有很多事需要说下。第一个也是最简单的,数组总是比对象快,数字访问比非数字访问更好。

for(var x = 0; x <arr.length; x ++){
    i = arr [x] .index;
}

上面的比下面的代码快

for (var x=0; x<100; x++) {
    i = obj[x].index;
}

上面的又比接下来的代码快

var keys = Object.keys(obj);
for(var x = 0; x <keys.length; x ++){
  i = obj [keys [x]]。index;
}

http://jsperf.com/array-vs-object-perf-demo
此外,请注意,在基于V8的现代浏览器中,具有很少属性的对象表现得明显更快,所以请将属性数量保持在最低限度。
还要注意,JavaScript能让你在数组中混合类型,但这并不是一个好主意:

var oneType = [1,2,3,4,5,6]
var multiType = [“string”,1,2,3,{a:'x'}]

第二次的操作明显比第一个慢得多,不仅仅是因为逻辑更复杂。
http://jsperf.com/array-types-compare-perf

还要避免使用删除。例如,给出:

var arr = [1,2,3,4,5,6];
var arrDelete = [1,2,3,4,5,6];
delete arrDelete [3];

任何arrDelete的迭代都会比arr迭代慢。
http://jsperf.com/delet-is-slow
这将在数组中创建一个undefined值,从而使操作效率更低。

5重要概念

刚才我们已经讨论了JavaScript的性能,这对于理解一些关键的angular概念很重要。

5.1 Scopes 和 Digest 循环

在angular核心,angular Scopes只是简单JavaScript对象。他们遵循预定义的原型继承方案,对此的深入讨论超出了本文的范围。与上述相关的是,小Scopes将比大Scopes更快。
在这一点上可以做出的另一个结论是,任何时间创建新的Scope,垃圾收集器将增加更多的值以便稍后回收。
一般来说,Digest循环对编写Angular JS应用程序和性能尤其重要。实际上,每个Scope都存储$$watchers函数的数组。
每次在一个Scope值上,或者一个绑定在DOM插值,一个ng-repeat,ng-switch,ng-if或者任何其他DOM属性/元素调用$watch,一个函数将被添加到$$watchers数组的最内层Scope。

当scope里面的任何值发生变化时,$$watchers数组中的所有watchers将触发,如果其中任何一个修改了观察值,则它们将会再次全部触发。 这将持续到$$watchers数组的不再改变并完整传递,或者AngularJS抛出异常。
另外,如果不是Angular代码运行$scope.$apply(),这将立即触发digest 循环。
最后要注意的是,$ scope.evalAsync()将在异步循环中运行代码,该循环不会触发另一个digest 循环,并且将在当前/下一个digest 循环结束时运行。

6 常见问题:设计时要注意

6.1大对象和服务器调用。

那么所有这些教导我们什么呢?第一个是考虑我们的数据模型,并限制对象的复杂性。这对于从服务器返回的对象尤其重要。
也就是说,返回整个数据库行,并且强制性的使用.toJson()是非常简单而且诱人的。这不够健壮:请不要这样做。
而是使用自定义序列化程序只返回Angular应用程序必须要用到的keys子集。

6.2观察函数

另一个常见的问题是在观察者或绑定中使用函数。不要将任何东西(ng-show,ng-repeat等)直接绑定到一个函数上。不要直接观察函数结果。这个函数将在每个digest 循环中运行,可能会减慢应用程序的爬网速度。

6.3观察对象

类似地,Angular提供了通过将第三个可选的true参数传递给scope.$watch来观察整个对象的能力。这是一个很糟糕的主意。一个更好的解决方案是依靠服务和对象引用在scopes之间传播对象更改。

7列表问题

7.1长列表

尽可能避免长列表。 ng-repeat会执行一些非常重的DOM操作(更不用说污染的$$watchers),所以尝试并保持渲染数据的列表尽量小,无论是通过分页还是无限滚动。

7.2过滤器

尽可能避免使用过滤器。它们在每个digest 循环运行两次,一次是在有任何更改时,另一次是收集进一步更改,并且实际上没有从内存中删除收集的任何部分,而只是使用CSS屏蔽过滤的项。
这使$ index无效,因为它不再对应于实际的数组索引,而是排序的数组索引。它也阻止您放弃所有列表的scopes。

7.3更新ng-repeat

当使用ng-repeat时,避免全局列表刷新也很重要。ng-repeat将填充一个$$ hashKey属性并标识该集合中的项目。这意味着,做一些像scope.listBoundToNgRepeat = serverFetch()这样的事情将导致整个列表的重新计算,导致运行外部程序并且观察者为每个单独的元素触发。这是一个非常昂贵、耗性能的。
这有两种方法。一个是在过滤集上维护两个集合和ng-repeat(更通用的,需要自定义同步逻辑,因此算法上更复杂和更少可维护),另一个是使用track by来指定自己的key(需要Angular 1.2+,略少通用,不需要自定义同步逻辑)。

简而言之:

scope.arr = mockServerFetch();

会慢于:

 var a = mockServerFetch();
    for(var i = scope.arr.length - 1; i >=0; i--){
      var result = _.find(a, function(r){
        return (r && r.trackingKey == scope.arr[i].trackingKey);
      });
      if (!result){
        scope.arr.splice(i, 1);
      } else {
        a.splice(a.indexOf(scope.arr[i]), 1);
      } 
    }
    _.map(a, function(newItem){
      scope.arr.push(newItem);
     });

这将比简单地添加更慢:

<div ng-repeat =“a in arr track by a.trackingKey”>

代替:

<div ng-repeat =“a in arr”>

这里可以找到这三种方法的全功能演示
只需在三个选项之间点击并要求重新获取就可以很好地显示出来。需要注意的是,track by方法仅在这个字段可以保证在循环对象上唯一时才起作用。对于服务器数据,id属性用作自然跟踪器。如果这不可能,不幸的是,自定义同步逻辑是唯一的办法。

8 渲染问题

Angular应用程序慢的常见原因是ng-hide和ng-show 以及 ng-if或ng-switch的不正确使用。这种区别是不容易的,并且对性能的重要性不能夸大。
ng-hide和ng-show只是切换CSS display 属性。这在实践中意味着,任何显示或隐藏的东西仍然在页面上,尽管看不见。任何scopes 将存在,所有的$$watchers都将触发等。
ng-if和ng-switch实际上完全删除或添加DOM。用ng-if删除的东西将没有scope。虽然性能优势应该是显而易见的,但是有一个需要抓住的点。具体来说,切换显示/隐藏比较便宜,但切换if / switch相对较贵。不幸的是,这导致了需要在一个个用例中权衡。作出这个决定需要回答的问题是:
这个变化有多频繁? (越频繁,ng-if 越糟糕)。
scope有多重? (越重,ng-if更适合)。

9。Digest 循环问题

9.1绑定

尝试并尽量减少绑定。从Angular 1.3开始,有一个新的语法,单向绑定 {{::scopeValue}}。这将从scope添加一次,而不向观察者数组添加观察器。

9.2 $digest() 和$apply()

scope.$apply是一个强大的工具,允许您将Angular外的值引入到应用程序中。在所有事件(ng-click等)下,它会触发。问题在于,scope.$apply从$rootScope开始,并遍历整个scope链,导致每个scope都会触发每个观察者。
scope.$digest 则起始于调用它的具体scope,只从那里向下传播。性能优势应该是相当明显的。当然,任何父级scopes 将不会收到此更新,直到下一个digest 循环。

9.3 $watch()

scope.$watch()已经讨论过几次了。一般来说,scope.$watch是一个表现糟糕的架构。当服务和引用绑定的某些组合在较低的开销时也能达到相同的结果,并且开销更少。很少有不能达到相同结果的情况。如果您必须创建一个观察者,请始终记住在第一时间解除绑定。您可以通过调用$watch函数来解除绑定。

var unbinder = scope.$watch('scopeValueToBeWatcher', function(newVal, oldVal){});
unbinder(); //this line removes the watch from $$watchers.

如果你不能尽早解绑,请记得在$on('$destroy')中解绑。

9.4 $on, $broadcast , 和 $emit

像$watch一样,这些都是缓慢的,因为事件(潜在地)必须遍及整个scope 层次结构。除此之外,GOTO还可以让您的应用程序成为一个复杂的调试问题。幸运的是,像$watch一样,他们可以调用返回的函数解绑(请记住在$on('$destroy') 中解除绑定),并且几乎可以避免使用服务和scope 继承。

9.5 $ destroy

如上所述,您应该总是明确地调用 $on('$destroy'),解除所有观察者和事件侦听器的绑定,并取消任何$timeout或其他异步正在进行的交互的实例。这不仅是确保安全的良好做法,更快地标示垃圾收集的scope 。不这样做会让他们在后台运行,浪费你的CPU和RAM。
特别重要的是要记住在$destroy函数调用中取消绑定在directives元素上定义的任何DOM事件侦听器。否则会导致旧版浏览器中的内存泄漏,并在现代浏览器中减慢您的垃圾收集器。一个非常重要的推论是在删除DOM之前调用scope.$destroy。

9.6 $evalAsync

scope.$evalAsync是一个强大的工具,可以让您在当前digest 循环结束时将操作排队执行,而不会使另一个digest 循环的scope 变脏。需要根据具体情况考虑这一点,但是,如果这是预期的效果,evalAsync可以大大提高页面的性能。

10指令问题

10.1独立作用域和嵌入

独立作用域和嵌入是Angular最令人兴奋的事情。它们允许构建可重复使用的封装组件,它们在语法和概念上都很优雅,是Angular的核心部分。
但是,他们也是需要权衡的。默认情况下,指令不会创建一个作用域,而是使用与其父元素相同的作用域。通过使用Isolate Scope或Transclusion创建新的scope,我们会创建一个新对象来跟踪,并添加新的观察者,这减慢我们的应用程序。所以在使用这些技术之前,请先停下来思考。

10.2编译周期

指令的compile函数在scope 附加之前运行,是运行任何DOM操作(例如绑定事件)的理想场所。 从性能的角度来看,重要的是,传递给编译函数的元素和属性表示原始的html模板,在进行任何angular的更改之前。 这意味着在这里完成的DOM操作将运行一次,并始终传播。 经常被忽略的另一个重点是prelink和postlink之间的区别。 简而言之,prelinks 从外而内运行,postlinks 而从内而外运行。 因此,prelinks提供轻微的性能提升,因为当父级修改prelink中的scope时,它们会阻止内部指令运行第二个digest 循环。 但是,子DOM可能是不可用的。

11 DOM事件问题

Angular提供了许多预先定义的DOM事件指令。 ng-click,ng-mouseenter,ng-mouseleave等。 所有这些调用scope.$apply() 每当发生事件时。 一个更有效的方法是直接与addEventListener绑定,然后根据需要使用scope.$digest。

12总结

12.1 AngularJS:糟粕

  • ng-click 和 other DOM events
  • scope.$watch
  • scope.$on
  • Directive postLink
  • ng-repeat
  • ng-show and ng-hide

12.2 AngularJS:精华

  • track by
  • :: 单次绑定
  • compile 和 preLink
  • $evalAsync
  • Services, scope inheritance, passing objects by reference
  • $destroy
  • unbinding watches 和 event listeners
  • ng-if 和 ng-switch

推荐阅读更多精彩内容