AngularJS渲染性能分析

作者:Jiang, Jilin

AngularJS中,通过数据绑定。能够十分方便的构建页面。可是当面对复杂的循环嵌套结构时,渲染会遇到性能瓶颈。今天,我们将通过一些列实验,来測试AngularJS的渲染性能,对照ng-show。ng-if的使用场景。并对优化进行简要分析。

只是在此之前,我们须要先简单过一遍AngularJS相关的代码:

$apply: function(expr) {
  try {
    beginPhase(‘$apply‘);
    try {
      return this.$eval(expr);
    } finally {
      clearPhase();
    }
  } catch (e) {
    $exceptionHandler(e);
  } finally {
    try {
      $rootScope.$digest();
    } catch (e) {
      $exceptionHandler(e);
      throw e;
    }
  }
},

beginPhase和clearPhase用于对$rootScope.$$phase进行锁定。假设发现反复进入$apply阶段则抛出异常。以免出现死循环。

$eval: function(expr, locals) {
  return $parse(expr)(this, locals);
},

$parse调用的是$ParseProvider。

因为之后的实验expr不传值。所以$ParseProvider会直接返回空函数noop() {}。

因此我们就不做详细的$ParseProvider内容分析了。

在运行完$eval后。会调用$digest方法。

让我们看看$digest里有些什么:

$digest: function() {
  var watch, value, last,
      watchers,
      length,
      dirty, ttl = TTL,
      next, current, target = this,
      watchLog = [],
      logIdx, logMsg, asyncTask;

  beginPhase(‘$digest‘);
  // Check for changes to browser url that happened in sync before the call to $digest
  $browser.$$checkUrlChange();

  if (this === $rootScope && applyAsyncId !== null) {
    // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then
    // cancel the scheduled $apply and flush the queue of expressions to be evaluated.
    $browser.defer.cancel(applyAsyncId);
    flushApplyAsync();
  }

  lastDirtyWatch = null;

  do { // "while dirty" loop
    dirty = false;
    current = target;

    while (asyncQueue.length) {
      try {
        asyncTask = asyncQueue.shift();
        asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals);
      } catch (e) {
        $exceptionHandler(e);
      }
      lastDirtyWatch = null;
    }

    traverseScopesLoop:
    do { // "traverse the scopes" loop
      if ((watchers = current.$$watchers)) {
        // process our watches
        length = watchers.length;
        while (length--) {
          try {
            watch = watchers[length];
            // Most common watches are on primitives, in which case we can short
            // circuit it with === operator, only when === fails do we use .equals
            if (watch) {
              if ((value = watch.get(current)) !== (last = watch.last) &&
                  !(watch.eq
                      ?

equals(value, last)
                      : (typeof value === ‘number‘ && typeof last === ‘number‘
                         && isNaN(value) && isNaN(last)))) {
                dirty = true;
                lastDirtyWatch = watch;
                watch.last = watch.eq ? copy(value, null) : value;
                watch.fn(value, ((last === initWatchVal) ? value : last), current);
                if (ttl < 5) {
                  logIdx = 4 - ttl;
                  if (!watchLog[logIdx]) watchLog[logIdx] = [];
                  watchLog[logIdx].push({
                    msg: isFunction(watch.exp) ?

‘fn: ‘ + (watch.exp.name || watch.exp.toString()) : watch.exp,
                    newVal: value,
                    oldVal: last
                  });
                }
              } else if (watch === lastDirtyWatch) {
                // If the most recently dirty watcher is now clean, short circuit since the remaining watchers
                // have already been tested.
                dirty = false;
                break traverseScopesLoop;
              }
            }
          } catch (e) {
            $exceptionHandler(e);
          }
        }
      }

      // Insanity Warning: scope depth-first traversal
      // yes, this code is a bit crazy, but it works and we have tests to prove it!
      // this piece should be kept in sync with the traversal in $broadcast
      if (!(next = ((current.$$watchersCount && current.$$childHead) ||
          (current !== target && current.$$nextSibling)))) {
        while (current !== target && !(next = current.$$nextSibling)) {
          current = current.$parent;
        }
      }
    } while ((current = next));

    // `break traverseScopesLoop;` takes us to here

    if ((dirty || asyncQueue.length) && !(ttl--)) {
      clearPhase();
      throw $rootScopeMinErr(‘infdig‘,
          ‘{0} $digest() iterations reached. Aborting!\n‘ +
          ‘Watchers fired in the last 5 iterations: {1}‘,
          TTL, watchLog);
    }

  } while (dirty || asyncQueue.length);

  clearPhase();

  while (postDigestQueue.length) {
    try {
      postDigestQueue.shift()();
    } catch (e) {
      $exceptionHandler(e);
    }
  }
},

相同的,调用beginPhase改变阶段。

$browser.$$checkUrlChange()用于检測url是否变更。这次我们也用不到:

function fireUrlChange() {
  if (lastBrowserUrl === self.url() && lastHistoryState === cachedState) {
    return;
  }

  lastBrowserUrl = self.url();
  lastHistoryState = cachedState;
  forEach(urlChangeListeners, function(listener) {
    listener(self.url(), cachedState);
  });
}

接着进行$rootScope和applyAsyncId推断。假设是根Scope而且存在异步apply请求。则调用$eval并把队列清空。也不是本次须要用到的部分。

进入循环,asyncQueue保存了$evalAsync方法的数据。

用不到。

之后设置了一个断点,用于跳出内部循环:

traverseScopesLoop:

循环内推断是否存在$$watchers列表,然后对watch单元进行变更匹配。每一个页面的数据绑定都会相应到一个watch单元。此处会检查是否watch是深匹配,假设为真会调用equals方法进行递归检查,假设watch了一个巨大的对象。那么equals会十分消耗性能。反之,则会检查是否是NaN,js中NaN != NaN。然而假设原值和现值都是NaN,事实上是没有变更过的。

if (watch) {
  if ((value = watch.get(current)) !== (last = watch.last) &&
      !(watch.eq
          ? equals(value, last)
          : (typeof value === ‘number‘ && typeof last === ‘number‘
             && isNaN(value) && isNaN(last)))) {

假设循环后已经发现watch单元原值和现值相等,会跳出循环。

再次又一次验证,目的是为了防止某个watch调用回调函数后。使得之前的watch现值发生变化。

而当中也设置了ttl循环计数。以免出现watch不断改变产生死循环的问题。

接着,就是著名的crazy凝视了:

// Insanity Warning: scope depth-first traversal
// yes, this code is a bit crazy, but it works and we have tests to prove it!
// this piece should be kept in sync with the traversal in $broadcast

此处会深度优先遍历,然后反复上面的检查。直到遍历结束。

作者非常贴心的标注一下循环结束了:

// `break traverseScopesLoop;` takes us to here

后面的代码就十分好懂了,clearPhase。然后处理DigestQueue结束循环。

之后检查ttl数值,假设ttl值超出了10次(预设值),则会抛出过多循环的异常。

实验

简单的过了一遍代码后。我们開始做一下性能測试:(注:因为不同机器配置性能不同,渲染时间仅作横向对照之用)

如今。如果我们拥有2个用户组。每组用户拥有1000个用户信息。用户信息例如以下:

[{name: "user1"}, {name:"user2"},...]

我们第一步做最简单未经过优化的渲染:

<div>
   <div ng-repeat="user in userList">
      <label>Name</label>
      <p>{{user.name}}</p>
   </div>
</div>

切换分组渲染时间平均310ms左右。

track by

然后简单使用优化track by优化:

ng-repeat="user in userList track by $index"

第一次渲染260ms左右。之后切换耗费11ms左右。

效果不错。接着,我们比較不同长度的数组切换比較。如果用户组1长度仍然为1000,用户组2长度100:(下图中,状态1、2代表绑定数组的切换)


状态1\状态2


用户组1


用户组2


用户组1


~0.3ms


~111ms


用户组2


~175ms


~0.1ms

我们能够看出,元素动态创建/删除会极大影响渲染性能。

创建相同数量元素比删除相同数量元素更消耗性能。

ng-show

基于以上实验。我们能够非常easy想到。假设我们使用元素池,预先创建足量的元素。接着通过ng-show来动态调整显示的元素。这样性能是否会上升呢?

$scope.getTimes = function(n) {
return new Array(n);
};
<div ng-repeat="i in getTimes(1000) track by $index" ng-show="userList[$index]">
   <label>Name</label>
   <p>{{userList[$index].name}}</p>
</div>

状态1\状态2


用户组1


用户组2


用户组1


~1.3ms


~42ms


用户组2


~22ms


~1.0ms

能够发现。同组切换时间消耗少量添加。

可是相对的,异组切换性能大幅提升了。

这是因为web中,元素操作是十分消耗性能的操作。因而为了性能。我们须要尽可能避免元素的创建/删除。相同的,因为每次渲染,都会调用new Array和检查ng-show属性,从而导致了同组切换的时间添加了。

ng-if与ng-show

Angularjs中还有还有一个方法ng-if,它是仅仅有满足表达式条件才会变更元素。对于用户组切换,其毫无疑问会创建/删除元素。只是在此,我还是把数据罗列一下:

<div ng-repeat="i in getTimes(1000) track by $index" ng-if="userList[$index]">
   <label>Name</label>
   <p>{{userList[$index].name}}</p>
</div>

状态1\状态2


用户组1


用户组2


用户组1


~11ms


~250ms


用户组2


~300ms


~5.5ms

能够看出,使用缓存+ng-if。性能消耗会比原本没有track by更消耗性能。

那么ng-if的适用场景是什么?是否全部的ng-if都适合被ng-show取代呢?让我们接下去继续看看列子。

 

组合

首先。我们对照一下有无缓存的初始化1000条数据的时间。


有缓存


无缓存


用户组1


~276ms


~240ms


用户组2


~278ms


~36ms

如今,我们如果用户有一个id属性。UI中,依据id是除以5的余数来做不同的渲染。规则例如以下:


余数


渲染元素


0


画一个2*2的table


1


显示一个长度为5的ul li列表


2


显示一个checkbox的input


3


显示一个textarea


4


显示一个text input

你可能已经看出我的想法了,我们的目的在于測试。假设存在多个不同渲染方式的情况下,是否适合使用ng-show。我们来看一下,(ng-switch近似ng-if,我们一起增加对照)

<div ng-repeat="user in userList track by $index">
   <label>Name</label>
   <p>{{user.name}}</p>

   <div ng-show="user.id % 5 === 0">
      <table>
         <tbody>
            <tr>
               <th>11</th>
               <th>12</th>
            </tr>
            <tr>
               <th>21</th>
               <th>22</th>
            </tr>
         </tbody>
      </table>
   </div>

   <div ng-show="user.id % 5 === 1">
      <ul>
         <li>1</li>
         <li>2</li>
         <li>3</li>
         <li>4</li>
         <li>5</li>
      </ul>
   </div>

   <div ng-show="user.id % 5 === 2">
      <input type="checkbox" />
   </div>

   <div ng-show="user.id % 5 === 3">
      <textarea></textarea>
   </div>

   <div ng-show="user.id % 5 === 4">
      <input type="text" />
   </div>
</div>
<div ng-repeat="user in userList track by $index">
   <label>Name</label>
   <p>{{user.name}}</p>

   <div ng-if="user.id % 5 === 0">
      <table>
         <tbody>
            <tr>
               <th>11</th>
               <th>12</th>
            </tr>
            <tr>
               <th>21</th>
               <th>22</th>
            </tr>
         </tbody>
      </table>
   </div>

   <div ng-if="user.id % 5 === 1">
      <ul>
         <li>1</li>
         <li>2</li>
         <li>3</li>
         <li>4</li>
         <li>5</li>
      </ul>
   </div>

   <div ng-if="user.id % 5 === 2">
      <input type="checkbox" />
   </div>

   <div ng-if="user.id % 5 === 3">
      <textarea></textarea>
   </div>

   <div ng-if="user.id % 5 === 4">
      <input type="text" />
   </div>
</div>

ng-show


ng-if


ng-switch


用户组1


~557ms


~766ms


~858ms

接着,測试切换:


ng-show


ng-if


ng-switch


组1->组2


~260ms


~257ms


~261ms


组2->组1


~430ms


~470ms


~560ms

好像ng-show各项数值都优于ng-if与ng-switch。只是还没完,我们继续改动样例。

为用户加入下面几个属性,相应绑定于之前定义的元素(m,n初始化时伪随机生成以便于測试对照数值):


属性


描写叙述


matrix


一个m*n的数组


list


一个长度为n的列表


desc


string


checked


boolean

<div ng-repeat="user in userList track by $index">
   <label>Name</label>
   <p>{{user.name}}</p>

   <div ng-show="user.id % 5 === 0">
      <table>
         <tbody>
            <tr ng-repeat="line in user.matrix track by $index">
               <th ng-repeat="val in line track by $index">{{val}}</th>
            </tr>
         </tbody>
      </table>
   </div>

   <div ng-show="user.id % 5 === 1">
      <ul>
         <li ng-repeat="val in user.list track by $index">{{val}}</li>
      </ul>
   </div>

   <div ng-show="user.id % 5 === 2">
      <input type="checkbox" ng-checked="user.checked" />
   </div>

   <div ng-show="user.id % 5 === 3">
      <textarea ng-model="user.desc"></textarea>
   </div>

   <div ng-show="user.id % 5 === 4">
      <input type="text" ng-model="user.desc" />
   </div>
</div>
<div ng-repeat="user in userList track by $index">
   <label>Name</label>
   <p>{{user.name}}</p>

   <div ng-if="user.id % 5 === 0">
      <table>
         <tbody>
            <tr ng-repeat="line in user.matrix track by $index">
               <th ng-repeat="val in line track by $index">{{val}}</th>
            </tr>
         </tbody>
      </table>
   </div>

   <div ng-if="user.id % 5 === 1">
      <ul>
         <li ng-repeat="val in user.list track by $index">{{val}}</li>
      </ul>
   </div>

   <div ng-if="user.id % 5 === 2">
      <input type="checkbox" ng-checked="user.checked" />
   </div>

   <div ng-if="user.id % 5 === 3">
      <textarea ng-model="user.desc"></textarea>
   </div>

   <div ng-if="user.id % 5 === 4">
      <input type="text" ng-model="user.desc" />
   </div>
</div>

ng-show


ng-if


ng-switch


用户组1


~4678ms


~1800ms


~1990ms

是不是大吃一惊?原因非常easy,因为ng-show仅仅是隐藏元素。

可是实际的数据绑定仍旧会被运行。

尽管在页面上看不到,可是元素绑定的数据还是一并更改了:

通过以上实验,我们非常easy分析出。当页面布局简单时,能够通过ng-show+cachelist来实现高速的数据切换。而当元素组件存在大量元素变化的时候,使用ng-if/ng-switch来避免多余的元素绑定。

通过两者结合的方式,能够使得程序在初始化和动态变化的时候保持更好的性能。相同的,在事件处理中。ng-if相较于ng-show会更有利于性能,可是假设事件绑定不多,使用ng-show则更佳。

时间: 2024-08-10 02:10:38

AngularJS渲染性能分析的相关文章

AngularJS开发者常犯的10个错误

Mark Meyer是一个有超过一年angular.js实际开发经验的full stack软件工程师. Mark拥有多种语言的开发经验,从基于C的服务器应用,基于Rails的web应用到使用Swift开发的IOS应用. 简介 AngularJS是目前最流行的Javascript框架之一,AngularJS的目标之一是简化开发过程,这使之非常善于构建小型app原型,但它也能够用于开发功能全面的客户端应用.便于开发,特性广泛以及出众的性能使其被广泛使用,然而,大量常见陷阱也随之而来.以下这份列表摘取

AngularJS开发最常犯的10个错误

简介 AngularJS是目前最为活跃的Javascript框架之一,AngularJS的目标之一是简化开发过程,这使得AngularJS非常善于构建小型app原型,但AngularJS对于全功能的客户端应用程序同样强大,它结合了开发简便,特性广泛和出众的性能,使其被广泛使用.然而,大量使用也会产生诸多误区.以下这份列表摘取了常见的一些AngularJS的错误用法,尤其是在app开发过程中. 1. MVC目录结构 AngularJS,直白地说,就是一个MVC框架.它的模型并没有像backbone

AngularJS开发人员最常犯的10个错误

AngularJS是目前最为活跃的Javascript框架之一,AngularJS的目标之一是简化开发过程,这使得AngularJS非常善于构建小型app原型,但AngularJS对于全功能的客户端应用程序同样强大,它结合了开发简便,特性广泛和出众的性能,使其被广泛使用.然而,大量使用也会产生诸多误区.以下这份列表摘取了常见的一些AngularJS的错误用法,尤其是在app开发过程中. 1. MVC目录结构 AngularJS,直白地说,就是一个MVC框架.它的模型并没有像backbone.js

angularJs中关于ng-class的三种使用方式说明

在开发中我们通常会遇到一种需求:一个元素在不同的状态需要展现不同的样子. 而在这所谓的样子当然就是改变其css的属性,而实现能动态的改变其属性值,必然只能是更换其class属性 这里有三种方法: 第一种:通过数据的双向绑定(不推荐) 第二种:通过对象数组 第三种:通过key/value 下面简单说下这三种: 第一种:通过数据的双向绑定 实现方式: function changeClass(){   $scope.className = "change2"; } <div clas

Node.js 使用angularjs取得Nodejs http服务端返回的JSON数组示例

server.js代码: // 内置http模块,提供了http服务器和客户端功能(path模块也是内置模块,而mime是附加模块) var http=require("http"); // 创建服务器,创建HTTP服务器要调用http.createServer()函数,它只有一个参数,是个回调函数,服务器每次收到http请求后都会调用这个回调函数.服务器每收到一条http请求,都会用新的request和response对象触发请求函数. var server=http.createS

Bootstrap + AngularJS+ Ashx + SQL Server/MySQL

去年年底12月,为适应移动端浏览需求,花了1个月时间学习Bootstrap,并将公司ASP网站重构成ASP.NET. 当时采取的网站架构: Bootstrap + jQuery + Ashx + SQL Server 时间紧,没人带,只能硬着头皮,最后如期完成,但是也遗留了几个问题. 问题: 1.页面查询条件太复杂,太多的checkbox,jQuery操作DOM虽然方便,但是组合成json提交给后端还是比较麻烦,有没有天然支持json的前端框架或者脚本语言? html控件做的任何修改,都自动保存

一招制敌 - 玩转 AngularJS 指令的 Scope (作用域),讲得特别好

学习了AngularJS挺长时间,最近再次回首看看指令这部分的时候,觉得比自己刚开始学习的时候理解的更加深入了,尤其是指令的作用域这部分. 步入正题: 每当一个指令被创建的时候,都会有这样一个选择,是继承自己的父作用域(一般是外部的Controller提供的作用域或者根作用域($rootScope)),还是创建一个新的自己的作用域,当然AngularJS为我们指令的scope参数提供了三种选择,分别是:false,true,{}:默认情况下是false. scope = false 首先我们来看

前端angularJS利用directive实现移动端自定义软键盘的方法

最近公司项目的需求上要求我们iPad项目上一些需要输入数字的地方用我们自定义的软键盘而不是移动端设备自带的键盘,刚接到需求有点懵,因为之前没有做过,后来理了一下思路发现这东西也就那样.先看一下实现之后的效果: 实现的效果就是当点击页面中需要弹出软键盘的时候软键盘弹出,浮在页面的中间,和模态框一样的效果,可以在软键盘中输入任何数字,附带的功能有小数点.退格.清空.确定等功能.当在键盘上点击数字的时候页面中的表单中实时的添加对应的数字,上图中可以看到. 产品经理那边给的原因是iPad屏幕本来就小,如

AngularJs自定义过滤器filter

AngularJs自带有很多过滤器,现在Insus.NET演示一个自定义的过滤器,如实现一个数据的平方. 本演示是在ASP.NET MVC环境中进行. 创建一个app: 创建一个控制器: 接下来是重点,创建一个过滤器,例子中的过滤器是实现一个数值的平方. 以上的所指的App,控制器和过滤器均是依Angularjs而言. 下面是ASP.NET MVC的视图,实现数据过滤: 程序运行结果: