11 Tips to Improve AngularJS Performance

I am new to Angular (even though I am not new to the web development), so please take everything that I am about to say with a grain of salt. That being said, I watched a lot of talks and read a lot of articles relevant to Angular performance, and this post is the summary of my findings.

Please make sure to check out some other posts: My Interview at Uber, 19 Tips For Everyday Git Use, Simple Guide to Finding a JavaScript Memory Leak in Node.js and I Tried To Virtually Stalk Mark Zuckerberg.

Table of Contents

  1. Minimize/Avoid Watchers
  2. Avoid ng-repeat. If you have to use ng-repeat use infinite scrolling or pagination
  3. Use Bind once when possible
  4. Use $watchCollection instead of $watch (with a 3rd parameter)
  5. Avoid repeated filters and cache data whenever possible
  6. Debounce ng-model
  7. Use ng-if instead of ng-show (but confirm that ng-if is actually better for your case
  8. Use console.time to benchmark your functions
  9. Use native JavaScript or Lodash for slow functions
  10. Use Batarang to benchmark your watchers
  11. Use Chrome Timeline and Profiler to identify performance bottlenecks

1. Minimize/Avoid Watchers

Usually, if your Angular app is slow, it means that you either have too many watcher, or those watchers are working harder then they should.

Angular uses dirty checking to keep track of all the changes in app. This means it will have to go through every watcher to check if they need to be updated (call the digest cycle). If one of the watcher is relied upon by another watcher, Angular would have to re-run the digest cycle again, to make sure that all of the changes has propagated. It will continue to do so, until all of the watchers have been updated and app has stabilized.

Even though running JavaScript in modern browsers is really fast, in Angular it is fairly easy to add so many watchers that you app will slow down to a crawl.

Keep in mind the following when implementing or refactoring an Angular app.
http://www.codelord.net/2014/06/17/angular-performance-101-slides/

  1. Watches are set on:

    • $scope.$watch
    • {{ }} type bindings
    • Most directives (i.e. ng-show)
    • Scope variables scope: { bar: '='}
    • Filters {{ value | myFilter }}
    • ng-repeat
  2. Watchers (digest cycle) run on

    • User action (ng-click etc). Most built in directives will call $scope.apply upon completion which triggers the digest cycle.
    • ng-change
    • ng-model
    • $http events (so all ajax calls)
    • $q promises resolved
    • $timeout
    • $interval
    • Manual call to $scope.apply and $scope.digest

2. Avoid ng-repeat. If you have to use ng-repeat use infinite scrolling or pagination

This was the biggest win for our app. I am not going to go into too much details, but I found the article bellow to be extremely helpful.

http://www.williambrownstreet.net/blog/2013/07/angularjs-my-solution-to-the-ng-repeat-performance-problem/

In addition to infinite scroll, make sure to use track by when possible. https://docs.angularjs.org/api/ng/directive/ngRepeat#tracking-and-duplicates

For example a unique step id, is a good value to track by when doing an ng-repeat.

<li ng-repeat="Task in Tasks track by Task.Id></li>

3. Use Bind once when possible

Angular 1.3 added :: notation to allow one time binding. In summary, Angular will wait for a value to stabilize after it’s first series of digest cycles, and will use that value to render the DOM element. After that, Angular will remove the watcher forgetting about that binding.
https://code.angularjs.org/1.3.15/docs/guide/expression#one-time-binding

<iframe id="cp_embed_QbWJRg" src="//codepen.io/akras14/embed/QbWJRg?height=268&theme-id=5767&slug-hash=QbWJRg&default-tab=result&user=akras14" scrolling="no" frameborder="0" height="268" allowtransparency="true" allowfullscreen="true" name="CodePen Embed" title="CodePen Embed 1" class="cp_embed_iframe " style="width: 100%; overflow: hidden;"></iframe>

If you are on pre 1.3 version of Angular you can use this library to achieve similar results:
https://github.com/Pasvaz/bindonce

4. Use $watchCollection instead of $watch (with a 3rd parameter)

$watch with only 2 parameters, is fast. However, Angular supports a 3rd parameter to this function, that can look like this: $watch('value', function(){}, true). The third parameter, tells Angular to perform deep checking, meaning to check every property of the object, which could be very expensive.

To address this performance issue, angular added $watchCollection('value', function(){}). $watchColleciton acts almost like $watch with a 3rd parameter, except it only checks the first layer of object’s properties, thus greatly improving the performance.

Official doc:
https://code.angularjs.org/1.3.15/docs/api/ng/type/$rootScope.Scope#$watchCollection

Helpful blog post:
http://www.bennadel.com/blog/2566-scope-watch-vs-watchcollection-in-angularjs.htm

5. Avoid repeated filters and cache data whenever possible

One time binding does not seem to play well with filters. There seems to be work arounds to make it work, but I think it’s cleaner and more intuitive to simply assign the needed value to a variable (or set it as a property on an object, if you are dealing with a lot of variables).

For example, instead of:

{{'DESCRIPTION' | translate }}

You can do:
– In JavaScript $scope.description: $translate.instant('DESCRIPTION')
– In HTML {{::description}}

Or instead of: {{step.time_modified | timeFormatFilter}}

  • In JavaScript

<textarea wrap="soft" class="crayon-plain print-no" data-settings="dblclick" readonly="" style="tab-size: 4; font-size: 14px !important; line-height: 20px !important; z-index: 0; opacity: 0; overflow: hidden;">var timeFormatFilter = $filter('timeFormatFilter');
step.time_modified = timeFormatFilter(step.time_modified);
</textarea>

col 1 col 2

1

2

3 | var timeFormatFilter = $filter('timeFormatFilter');

step.time_modified = timeFormatFilter(step.time_modified);

  • In HTML {{::Path.time_modified}}

6. Debounce ng-model

If you know there is going to be a lot of changes coming from an ng-model, you can de-bounce the input.

For example if you have a search input like Google, you can de-bounce it by setting the following ng-model option: ng-model-options="{ debounce: 250 }.

This will ensure that the digest cycle due to the changes in this input model will get triggered no more then once per 250ms .

https://docs.angularjs.org/api/ng/directive/ngModelOptions

7. Use ng-if instead of ng-show (but confirm that ng-if is actually better for your use case)

ng-show will render an element, and use display:none to hide it,
ng-if will actually removes the element from DOM, and will re-create it, if it’s needed.

You may need ng-show for an elements that toggles on an off often, but for 95% of the time, ng-if is a better way to go.

8. Use console.time to benchmark your functions

console.time is a great API, and I found it particularly helpful when debugging issues with Angular performance. I placed a number of those calls through out my code, to help me confirm that my re-factoring was in fact improving the performance.

https://developer.mozilla.org/en-US/docs/Web/API/Console/time

The API looks as such:

<textarea wrap="soft" class="crayon-plain print-no" data-settings="dblclick" readonly="" style="tab-size: 4; font-size: 14px !important; line-height: 20px !important; z-index: 0; opacity: 0; overflow: hidden;">console.time("TimerName");
//Some code
console.timeEnd("TimerName");
</textarea>

col 1 col 2

1

2

3

4 | console.time("TimerName");

//Some code

console.timeEnd("TimerName");

And here is a simple example:

<textarea wrap="soft" class="crayon-plain print-no" data-settings="dblclick" readonly="" style="tab-size: 4; font-size: 14px !important; line-height: 20px !important; z-index: 0; opacity: 0; overflow: hidden;">console.time("TimerName");
setTimeout(function(){
console.timeEnd("TimerName");
}, 100);
//In console $: TimerName: 100.324ms
</textarea>

col 1 col 2

1

2

3

4

5

6 | console.time("TimerName");

setTimeout(function(){

console.timeEnd("TimerName");

}, 100);

//In console $: TimerName: 100.324ms

Note: If console.time is not precise enough for your needs, you can get a more accurate reading using performance.now(). You will have to do your own math, if you choose to take this path.

https://docs.google.com/presentation/…

<textarea wrap="soft" class="crayon-plain print-no" data-settings="dblclick" readonly="" style="tab-size: 4; font-size: 14px !important; line-height: 20px !important; z-index: 0; opacity: 0; overflow: hidden;">totalTime = 0; count = 0;
var someFunction = function() {
var thisRunStartTime = performance.now();
count++;
// some code

// some more code
totalTime += performance.now() - thisRunStartTime;
};
console.log("Average time: " + totalTime/count);
</textarea>

col 1 col 2

1

2

3

4

5

6

7

8

9

10

11 | totalTime = 0; count = 0;

var someFunction = function() {

var thisRunStartTime = performance.now();

count++;

// some code

// some more code

totalTime += performance.now() - thisRunStartTime;

};

console.log("Average time: " + totalTime/count);

9. Use native JavaScript or Lodash for slow functions

Our app was already using lodash, so there was no overhead for me to use it in my optimization. If lodash was not include, I would probably try to re-write everything in native JavaScript.

In my tests I got a significant performance boost by simply re-writing some of the basic logic with lodash, instead of relying on built-in Angular methods (which have to account for much more generic use cases).

Maintainer of Lodash John-David Dalton is also a co-creator of https://jsperf.com/, and he is all about the performance. So I trust him and his library when it comes to speed.

10. Use Batarang to benchmark your watchers

Batarang is a great tool from the Angular team, and it was very helpful in my debugging efforts. It has a lot of useful features, but the one that was the most relevant to this use-case is the performance tab.

Make sure to get the stable version, which seems to work for the majority of users.
https://chrome.google.com/webstore/detail/angularjs-batarang-stable/niopocochgahfkiccpjmmpchncjoapek

Watch this video to get more insight into the Batarang.

<iframe width="560" height="315" src="https://www.youtube.com/embed/q-7mhcHXSfM" frameborder="0" allowfullscreen=""></iframe>

11. Use Chrome Timeline and Profiler to identify performance bottlenecks

I like to think of myself as a Chrome Dev Tools power user. But it’s not often that I get a to use the Timeline and Profiler views. In this project, both were extremely helpful.

Pro Tip: If you use console.time API (see tip #8), the time period will get highlighted on your timeline snapshot. So you can examine the exact time period that you care about the most.

https://developer.chrome.com/devtools/docs/timeline#user-produced-timeline-events

The timeline view, and the magic 60fps line is crucial. When I started on our project, the app was rendering full steam for 15 seconds or more, becoming almost completely unresponsive to the user.

After performance optimization, the app now fully renders in less then 2 seconds (note that the time scale is different), allowing users to freely interact with the user interface after a relatively short delay.

It is clear from looking at the image that the app could be further optimized. But even as is, I am very happy with the improvements to the user experience.

To get more experience with Timeline view, check out these web audits by Paul Irish:

https://docs.google.com/document/d/1K-mKOqiUiSjgZTEscBLjtjd6E67oiK8H2ztOiq5tigk/pub

Finally, I wanted to mentioned the Profiling tab in Chrome Dev tools, and the JavaScript CPU profiler in particular. It has 3 views:

1. Chart view is similar to the timeline, but it makes it a bit easier to jump to the source code of the function of interest.

2. Heavy (Bottom up view)
This view identifies heavy user functions, and shows you the reverse call stack to help pinpoint origination of the function. Note how $digest comes before the $apply, indicating the reverse order.

3. Tree (Top Down)
Exposes the functions from which the heavy consumption originated, and then you can drill down to find the offending function.

Also note the yellow triangle with a “!”, if you however over it, it will identify a potential optimization problem.

https://developer.chrome.com/devtools/docs/cpu-profiling

P.S. If you found these tips helpful, please consider reviewing this post on Amazon

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容