1介紹
無(wú)論您是為大型的舊應(yīng)用程序編寫(xiě)Angular,還是您已有的Angular應(yīng)用程序在變得龐大,性能是一個(gè)重要的方面。 了解AngularJS應(yīng)用程序減慢的原因以及如何在開(kāi)發(fā)過(guò)程中做出權(quán)衡很重要。 本文將介紹AngularJS的一些常見(jiàn)的性能問(wèn)題,以及如何避免和修復(fù)的建議。
1.1要求,假設(shè)
本文將會(huì)假定你對(duì)JavaScript語(yǔ)言和AngularJS有些熟悉。 當(dāng)使用特定版本的功能時(shí),它們將被調(diào)用。 為了充分利用這篇文章,最好是用過(guò)一段時(shí)間Angular,但還沒(méi)有認(rèn)真對(duì)待性能。
2衡量工具
2.1基準(zhǔn)測(cè)試
jsPerf是一個(gè)很棒的用于對(duì)代碼進(jìn)行基準(zhǔn)測(cè)試的工具。 我將在相關(guān)部分結(jié)尾給出具體的測(cè)試鏈接,以便閱讀。
2.2 分析
Chrome開(kāi)發(fā)工具有一個(gè)很好的JavaScript分析器。 我強(qiáng)烈推薦閱讀本系列 文章。
2.3 Angular Batarang
Angular Batarang是一個(gè)專(zhuān)注于angular的調(diào)試器,由Angular Core Team維護(hù),可在GitHub上獲得。
3軟件性能
決定是否高性能軟件有兩個(gè)根本原因。
第一個(gè)是算法時(shí)間復(fù)雜度。 解決這個(gè)問(wèn)題很大程度上超出了本文的討論范圍,一般來(lái)說(shuō),時(shí)間復(fù)雜度是程序需要做多少計(jì)算來(lái)取得結(jié)果的一個(gè)衡量標(biāo)準(zhǔn)。 計(jì)算的數(shù)量越大,程序越慢。 一個(gè)簡(jiǎn)單的例子是線性搜索與二進(jìn)制搜索。 線性搜索需要對(duì)同一組數(shù)據(jù)進(jìn)行更多計(jì)算,因此將會(huì)更慢。 有關(guān)時(shí)間復(fù)雜性的詳細(xì)討論,請(qǐng)參閱維基百科文章。
第二個(gè)原因就是算法空間復(fù)雜度。 這是計(jì)算機(jī)運(yùn)行算法需要多少“存儲(chǔ)空間”或內(nèi)存的衡量標(biāo)準(zhǔn)。 需要的內(nèi)存越多,解決方案越慢。 本文討論的大多數(shù)問(wèn)題在空間復(fù)雜性之下將會(huì)變得松動(dòng)。 詳細(xì)討論請(qǐng)看這里。
這句話(huà)不知道怎么翻譯,有知道的朋友請(qǐng)告知,謝謝。原文:
Most of the problems this article will talk to fall loosely under space complexity. For a detailed discussion, see here.
4 Javascript 性能
這里說(shuō)下關(guān)于JavaScript 性能的幾件事情,不一定局限于angular。
4.1 循環(huán)
避免在循環(huán)中調(diào)用。 如果循環(huán)內(nèi)的調(diào)用可以在循環(huán)之外執(zhí)行,那么把它放到循環(huán)之外將極大地加快您的系統(tǒng)。 例如:
var sum = 0;
for(var x = 0; x < 100; x++){
var keys = Object.keys(obj); sum = sum + keys[x];
}
會(huì)顯著慢于:
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訪問(wèn)
注意DOM訪問(wèn),這很重要。
angular.element( 'div.elementClass')
代價(jià)昂貴。雖然在AngularJS中出現(xiàn)這種問(wèn)題的幾率很小,但還是有必要了解這一點(diǎn)。這里要說(shuō)的第二件事就是在可能的情況下,DOM樹(shù)應(yīng)該保持較小。
最后,盡可能避免修改DOM和設(shè)置內(nèi)聯(lián)樣式。因?yàn)檫@會(huì)導(dǎo)致JavaScript重繪。重繪的深入討論超出了本文的范圍,但這里有一個(gè)很棒的參考。
4.3變量作用域和垃圾回收
盡可能?chē)?yán)格地將所有變量聲明為局部作用域,以使JavaScript垃圾回收器能夠更快地釋放內(nèi)存。
這句我是參照上下文推斷出來(lái)的,翻譯的可能有點(diǎn)問(wèn)題??煽丛模?/p>
Scope all variables as tightly as possible to allow the JavaScript garbage collector to free up your memory sooner rather then later.
這是造成JavaScript,特別是Angular緩慢,滯后,不響應(yīng)非常常見(jiàn)的原因。請(qǐng)注意以下問(wèn)題:
function demo(){
var b = {childFunction:function(){console.log('hi this is the child function')};
b.childFunction();
return b;
}
當(dāng)函數(shù)終止時(shí),將不再有對(duì)b可用的引用,垃圾回收器將釋放內(nèi)存。但是,如果在其他地方有這樣的一行:
var cFunc = demo();
我們現(xiàn)在將對(duì)象綁定到一個(gè)變量并保持對(duì)它的引用,從而防止垃圾收集器清理它。雖然這可能是必要的,但重要的是要注意這對(duì)對(duì)象引用的影響。
4.4數(shù)組和對(duì)象
這里有很多事需要說(shuō)下。第一個(gè)也是最簡(jiǎn)單的,數(shù)組總是比對(duì)象快,數(shù)字訪問(wèn)比非數(shù)字訪問(wèn)更好。
for(var x = 0; x <arr.length; x ++){
i = arr [x] .index;
}
上面的比下面的代碼快
for (var x=0; x<100; x++) {
i = obj[x].index;
}
上面的又比接下來(lái)的代碼快
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
此外,請(qǐng)注意,在基于V8的現(xiàn)代瀏覽器中,具有很少屬性的對(duì)象表現(xiàn)得明顯更快,所以請(qǐng)將屬性數(shù)量保持在最低限度。
還要注意,JavaScript能讓你在數(shù)組中混合類(lèi)型,但這并不是一個(gè)好主意:
var oneType = [1,2,3,4,5,6]
var multiType = [“string”,1,2,3,{a:'x'}]
第二次的操作明顯比第一個(gè)慢得多,不僅僅是因?yàn)檫壿嫺鼜?fù)雜。
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的迭代都會(huì)比arr迭代慢。
http://jsperf.com/delet-is-slow
這將在數(shù)組中創(chuàng)建一個(gè)undefined值,從而使操作效率更低。
5重要概念
剛才我們已經(jīng)討論了JavaScript的性能,這對(duì)于理解一些關(guān)鍵的angular概念很重要。
5.1 Scopes 和 Digest 循環(huán)
在angular核心,angular Scopes只是簡(jiǎn)單JavaScript對(duì)象。他們遵循預(yù)定義的原型繼承方案,對(duì)此的深入討論超出了本文的范圍。與上述相關(guān)的是,小Scopes將比大Scopes更快。
在這一點(diǎn)上可以做出的另一個(gè)結(jié)論是,任何時(shí)間創(chuàng)建新的Scope,垃圾收集器將增加更多的值以便稍后回收。
一般來(lái)說(shuō),Digest循環(huán)對(duì)編寫(xiě)Angular JS應(yīng)用程序和性能尤其重要。實(shí)際上,每個(gè)Scope都存儲(chǔ)$$watchers函數(shù)的數(shù)組。
每次在一個(gè)Scope值上,或者一個(gè)綁定在DOM插值,一個(gè)ng-repeat,ng-switch,ng-if或者任何其他DOM屬性/元素調(diào)用$watch,一個(gè)函數(shù)將被添加到$$watchers數(shù)組的最內(nèi)層Scope。
當(dāng)scope里面的任何值發(fā)生變化時(shí),$$watchers數(shù)組中的所有watchers將觸發(fā),如果其中任何一個(gè)修改了觀察值,則它們將會(huì)再次全部觸發(fā)。 這將持續(xù)到$$watchers數(shù)組的不再改變并完整傳遞,或者AngularJS拋出異常。
另外,如果不是Angular代碼運(yùn)行$scope.$apply(),這將立即觸發(fā)digest 循環(huán)。
最后要注意的是,$ scope.evalAsync()將在異步循環(huán)中運(yùn)行代碼,該循環(huán)不會(huì)觸發(fā)另一個(gè)digest 循環(huán),并且將在當(dāng)前/下一個(gè)digest 循環(huán)結(jié)束時(shí)運(yùn)行。
6 常見(jiàn)問(wèn)題:設(shè)計(jì)時(shí)要注意
6.1大對(duì)象和服務(wù)器調(diào)用。
那么所有這些教導(dǎo)我們什么呢?第一個(gè)是考慮我們的數(shù)據(jù)模型,并限制對(duì)象的復(fù)雜性。這對(duì)于從服務(wù)器返回的對(duì)象尤其重要。
也就是說(shuō),返回整個(gè)數(shù)據(jù)庫(kù)行,并且強(qiáng)制性的使用.toJson()是非常簡(jiǎn)單而且誘人的。這不夠健壯:請(qǐng)不要這樣做。
而是使用自定義序列化程序只返回Angular應(yīng)用程序必須要用到的keys子集。
6.2觀察函數(shù)
另一個(gè)常見(jiàn)的問(wèn)題是在觀察者或綁定中使用函數(shù)。不要將任何東西(ng-show,ng-repeat等)直接綁定到一個(gè)函數(shù)上。不要直接觀察函數(shù)結(jié)果。這個(gè)函數(shù)將在每個(gè)digest 循環(huán)中運(yùn)行,可能會(huì)減慢應(yīng)用程序的爬網(wǎng)速度。
6.3觀察對(duì)象
類(lèi)似地,Angular提供了通過(guò)將第三個(gè)可選的true參數(shù)傳遞給scope.$watch來(lái)觀察整個(gè)對(duì)象的能力。這是一個(gè)很糟糕的主意。一個(gè)更好的解決方案是依靠服務(wù)和對(duì)象引用在scopes之間傳播對(duì)象更改。
7列表問(wèn)題
7.1長(zhǎng)列表
盡可能避免長(zhǎng)列表。 ng-repeat會(huì)執(zhí)行一些非常重的DOM操作(更不用說(shuō)污染的$$watchers),所以嘗試并保持渲染數(shù)據(jù)的列表盡量小,無(wú)論是通過(guò)分頁(yè)還是無(wú)限滾動(dòng)。
7.2過(guò)濾器
盡可能避免使用過(guò)濾器。它們?cè)诿總€(gè)digest 循環(huán)運(yùn)行兩次,一次是在有任何更改時(shí),另一次是收集進(jìn)一步更改,并且實(shí)際上沒(méi)有從內(nèi)存中刪除收集的任何部分,而只是使用CSS屏蔽過(guò)濾的項(xiàng)。
這使$ index無(wú)效,因?yàn)樗辉賹?duì)應(yīng)于實(shí)際的數(shù)組索引,而是排序的數(shù)組索引。它也阻止您放棄所有列表的scopes。
7.3更新ng-repeat
當(dāng)使用ng-repeat時(shí),避免全局列表刷新也很重要。ng-repeat將填充一個(gè)$$ hashKey屬性并標(biāo)識(shí)該集合中的項(xiàng)目。這意味著,做一些像scope.listBoundToNgRepeat = serverFetch()這樣的事情將導(dǎo)致整個(gè)列表的重新計(jì)算,導(dǎo)致運(yùn)行外部程序并且觀察者為每個(gè)單獨(dú)的元素觸發(fā)。這是一個(gè)非常昂貴、耗性能的。
這有兩種方法。一個(gè)是在過(guò)濾集上維護(hù)兩個(gè)集合和ng-repeat(更通用的,需要自定義同步邏輯,因此算法上更復(fù)雜和更少可維護(hù)),另一個(gè)是使用track by來(lái)指定自己的key(需要Angular 1.2+,略少通用,不需要自定義同步邏輯)。
簡(jiǎn)而言之:
scope.arr = mockServerFetch();
會(huì)慢于:
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);
});
這將比簡(jiǎn)單地添加更慢:
<div ng-repeat =“a in arr track by a.trackingKey”>
代替:
<div ng-repeat =“a in arr”>
這里可以找到這三種方法的全功能演示。
只需在三個(gè)選項(xiàng)之間點(diǎn)擊并要求重新獲取就可以很好地顯示出來(lái)。需要注意的是,track by方法僅在這個(gè)字段可以保證在循環(huán)對(duì)象上唯一時(shí)才起作用。對(duì)于服務(wù)器數(shù)據(jù),id屬性用作自然跟蹤器。如果這不可能,不幸的是,自定義同步邏輯是唯一的辦法。
8 渲染問(wèn)題
Angular應(yīng)用程序慢的常見(jiàn)原因是ng-hide和ng-show 以及 ng-if或ng-switch的不正確使用。這種區(qū)別是不容易的,并且對(duì)性能的重要性不能夸大。
ng-hide和ng-show只是切換CSS display 屬性。這在實(shí)踐中意味著,任何顯示或隱藏的東西仍然在頁(yè)面上,盡管看不見(jiàn)。任何scopes 將存在,所有的$$watchers都將觸發(fā)等。
ng-if和ng-switch實(shí)際上完全刪除或添加DOM。用ng-if刪除的東西將沒(méi)有scope。雖然性能優(yōu)勢(shì)應(yīng)該是顯而易見(jiàn)的,但是有一個(gè)需要抓住的點(diǎn)。具體來(lái)說(shuō),切換顯示/隱藏比較便宜,但切換if / switch相對(duì)較貴。不幸的是,這導(dǎo)致了需要在一個(gè)個(gè)用例中權(quán)衡。作出這個(gè)決定需要回答的問(wèn)題是:
這個(gè)變化有多頻繁? (越頻繁,ng-if 越糟糕)。
scope有多重? (越重,ng-if更適合)。
9。Digest 循環(huán)問(wèn)題
9.1綁定
嘗試并盡量減少綁定。從Angular 1.3開(kāi)始,有一個(gè)新的語(yǔ)法,單向綁定 {{::scopeValue}}。這將從scope添加一次,而不向觀察者數(shù)組添加觀察器。
9.2 $digest() 和$apply()
scope.$apply是一個(gè)強(qiáng)大的工具,允許您將Angular外的值引入到應(yīng)用程序中。在所有事件(ng-click等)下,它會(huì)觸發(fā)。問(wèn)題在于,scope.$apply從$rootScope開(kāi)始,并遍歷整個(gè)scope鏈,導(dǎo)致每個(gè)scope都會(huì)觸發(fā)每個(gè)觀察者。
scope.$digest 則起始于調(diào)用它的具體scope,只從那里向下傳播。性能優(yōu)勢(shì)應(yīng)該是相當(dāng)明顯的。當(dāng)然,任何父級(jí)scopes 將不會(huì)收到此更新,直到下一個(gè)digest 循環(huán)。
9.3 $watch()
scope.$watch()已經(jīng)討論過(guò)幾次了。一般來(lái)說(shuō),scope.$watch是一個(gè)表現(xiàn)糟糕的架構(gòu)。當(dāng)服務(wù)和引用綁定的某些組合在較低的開(kāi)銷(xiāo)時(shí)也能達(dá)到相同的結(jié)果,并且開(kāi)銷(xiāo)更少。很少有不能達(dá)到相同結(jié)果的情況。如果您必須創(chuàng)建一個(gè)觀察者,請(qǐng)始終記住在第一時(shí)間解除綁定。您可以通過(guò)調(diào)用$watch函數(shù)來(lái)解除綁定。
var unbinder = scope.$watch('scopeValueToBeWatcher', function(newVal, oldVal){});
unbinder(); //this line removes the watch from $$watchers.
如果你不能盡早解綁,請(qǐng)記得在$on('$destroy')中解綁。
9.4 $on, $broadcast , 和 $emit
像$watch一樣,這些都是緩慢的,因?yàn)槭录撛诘兀┍仨毐榧罢麄€(gè)scope 層次結(jié)構(gòu)。除此之外,GOTO還可以讓您的應(yīng)用程序成為一個(gè)復(fù)雜的調(diào)試問(wèn)題。幸運(yùn)的是,像$watch一樣,他們可以調(diào)用返回的函數(shù)解綁(請(qǐng)記住在$on('$destroy') 中解除綁定),并且?guī)缀蹩梢员苊馐褂梅?wù)和scope 繼承。
9.5 $ destroy
如上所述,您應(yīng)該總是明確地調(diào)用 $on('$destroy'),解除所有觀察者和事件偵聽(tīng)器的綁定,并取消任何$timeout或其他異步正在進(jìn)行的交互的實(shí)例。這不僅是確保安全的良好做法,更快地標(biāo)示垃圾收集的scope 。不這樣做會(huì)讓他們?cè)诤笈_(tái)運(yùn)行,浪費(fèi)你的CPU和RAM。
特別重要的是要記住在$destroy函數(shù)調(diào)用中取消綁定在directives元素上定義的任何DOM事件偵聽(tīng)器。否則會(huì)導(dǎo)致舊版瀏覽器中的內(nèi)存泄漏,并在現(xiàn)代瀏覽器中減慢您的垃圾收集器。一個(gè)非常重要的推論是在刪除DOM之前調(diào)用scope.$destroy。
9.6 $evalAsync
scope.$evalAsync是一個(gè)強(qiáng)大的工具,可以讓您在當(dāng)前digest 循環(huán)結(jié)束時(shí)將操作排隊(duì)執(zhí)行,而不會(huì)使另一個(gè)digest 循環(huán)的scope 變臟。需要根據(jù)具體情況考慮這一點(diǎn),但是,如果這是預(yù)期的效果,evalAsync可以大大提高頁(yè)面的性能。
10指令問(wèn)題
10.1獨(dú)立作用域和嵌入
獨(dú)立作用域和嵌入是Angular最令人興奮的事情。它們?cè)试S構(gòu)建可重復(fù)使用的封裝組件,它們?cè)谡Z(yǔ)法和概念上都很優(yōu)雅,是Angular的核心部分。
但是,他們也是需要權(quán)衡的。默認(rèn)情況下,指令不會(huì)創(chuàng)建一個(gè)作用域,而是使用與其父元素相同的作用域。通過(guò)使用Isolate Scope或Transclusion創(chuàng)建新的scope,我們會(huì)創(chuàng)建一個(gè)新對(duì)象來(lái)跟蹤,并添加新的觀察者,這減慢我們的應(yīng)用程序。所以在使用這些技術(shù)之前,請(qǐng)先停下來(lái)思考。
10.2編譯周期
指令的compile函數(shù)在scope 附加之前運(yùn)行,是運(yùn)行任何DOM操作(例如綁定事件)的理想場(chǎng)所。 從性能的角度來(lái)看,重要的是,傳遞給編譯函數(shù)的元素和屬性表示原始的html模板,在進(jìn)行任何angular的更改之前。 這意味著在這里完成的DOM操作將運(yùn)行一次,并始終傳播。 經(jīng)常被忽略的另一個(gè)重點(diǎn)是prelink和postlink之間的區(qū)別。 簡(jiǎn)而言之,prelinks 從外而內(nèi)運(yùn)行,postlinks 而從內(nèi)而外運(yùn)行。 因此,prelinks提供輕微的性能提升,因?yàn)楫?dāng)父級(jí)修改prelink中的scope時(shí),它們會(huì)阻止內(nèi)部指令運(yùn)行第二個(gè)digest 循環(huán)。 但是,子DOM可能是不可用的。
11 DOM事件問(wèn)題
Angular提供了許多預(yù)先定義的DOM事件指令。 ng-click,ng-mouseenter,ng-mouseleave等。 所有這些調(diào)用scope.$apply() 每當(dāng)發(fā)生事件時(shí)。 一個(gè)更有效的方法是直接與addEventListener綁定,然后根據(jù)需要使用scope.$digest。
12總結(jié)
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