前兩天因爲想用國內的 JS CDN,訪問到 staticfile(七牛提供的一個免費 CDN 服務),導致我的 Chrome 直接卡死了兩次,頁面關也關不掉,只能退出重啓。後來換到 Chrome Canary 才勉強可以訪問,用谷歌開發者工具一看,發現前端是用的 AngularJS ,大量的 ng-repeat
,5萬多 watcher
,沒有做任何性能優化,怎麼可能不卡。
今年以來一直在陸陸續續的優化公司基於 AngularJS 開發的應用,也積累了一些經驗,正好總結一下 AngularJS webapp 的性能優化指南。
1. 減少 watcher
我們知道 AngularJS 通過 髒檢查(digest cicle)來更新視圖,保持數據和視圖的同步,髒檢查的效率是和 watcher
的多少成正相關的,一般來說超過 2000 後就會明顯感覺到變慢,所以提高 AngularJS 性能的關鍵就是減少 watcher
的數量。
首先,我們要知道的是,什麼會產生 watcher
?
$scope.$watch
{ { stuff } }
類模板語法- 大多數指令 (比如
ng-show
、ng-if
) - Scope 變量
scope: { bar: '='}
- 過濾器
{ { value | myFilter } }
ng-repeat
指令
上面這些情況都會產生 watcher
,那麼問題來了,怎麼減少 watcher
呢?
- 使用單次綁定語法
{ {::} }
AngularJS 從1.3版本開始支持單向綁定語法::
,它可以明確的告訴 AngularJS 哪些綁定獲取到數據以後就不用關注了,這可以極大的減少watcher
的數量,尤其是在ng-repeat
內使用。 - 避免在
ng-repeat
中使用filter
可以先把數據過濾後再傳給ng-repeat
,這樣就能避免因爲過濾器產生的watcher
了。 - 儘可能的使用
ng-if
而不是ng-show
ng-if
可以從 Dom 中移除元素,觸發element.$destory()
,刪除ng-if
內的元素的watcher
。ng-show
仍然會 render 元素,只是設置樣式爲display:none
。 但是如果元素需要經常變動隱藏還是顯示,那麼使用ng-show
可能會更好,ng-show
會緩存 Dom,不需要重複解析。 - 使用
$watchCollection
替代$watch
2. 減少 digest
次數和範圍
減少 watcher
是從根本上解決問題,如果 watcher
的優化已經做到極致了,那麼這時候就應該換一種思路了。導致 AngularJS App 變慢的原因是 watcher
太多導致 digest
變慢,watcher
已經無法優化了,那麼就應該考慮從 digest
的下手了。
同樣,首先要知道的是,什麼情況下會觸發 AngularJS 髒檢查?
- 用戶行爲(
ng-click
、ng-change
、ng-model
,etc) $http
接口響應$q
promises resolved- 使用
$timeout
和$interval
- 你手動調用
$scope.$apply
或$scope.$digest
優化主要從兩個方向進行,減少髒檢查的次數 和 縮小髒檢查的範圍。
-
儘量使用
$scope.$digest
替代$scope.$apply
$scope.$digest
從當前 scope 向下進行髒檢查,而$scope.$apply
會觸發整個應用自頂向下進行髒檢查,所以,使用$scope.$digest
一般能大大的縮小髒檢查的範圍。 -
使用
$applyAsync
合併 http 請求
通常在 App 啓動的時候,會同時發起好幾個 http 請求,來獲取用戶權限或賬戶信息之類的信息,每次接口返回值的時候,都會觸發 AngularJS 的髒檢查。這時候,如果可以等到這幾個接口都返回以後,再觸發髒檢查,就能將髒檢查的數量由幾次減小到1次了。$httpProvider
的 useApplyAsync 方法就是來解決這個問題,它通過 $rootScope.$applyAsync 把大約同一時間(10ms左右)收到的返回值組合到一起處理。applyAsync
的實現機制其實就是事件循環,通過setTimeout(fn,0)
來延遲執行函數,可以參考我寫的《深入學習 Zone》瞭解更多。123app.config(function ($httpProvider) {$httpProvider.useApplyAsync(true)}) -
ng-model 防抖動( Debounce )
搜索框通常會監聽用戶的 keyup 事件來進行實時匹配推薦,如果每次用戶按下按鍵都調用接口,會出現多次連續的調用接口,導致連續的觸發 AngularJS 髒檢查,這樣很容易造成頁面卡頓。這時,可以通過ng-model
的debounce
參數來限制髒檢查的間隔,比如ng-model-options="{ debounce: 250 }
,限制每 250ms 內只進行一次髒檢查。 -
使用
$watchCollection
替代$watch
的第三個參數$watch
只會比較對象引用是否相同,如果新值和原始值指向同一個索引,那麼$digest
時就不會觸發回調函數。如果要監視對象的每個屬性,我們可以給$watch
傳入第三個參數true
,這樣 AngularJS 就會對對象進行深比較(使用angular.equals
),遍歷對象的每個值判斷是否發生了變化。但如果對象比較複雜,這樣做就會帶來很大的性能損耗。所以,AngularJS 提供了$watchCollection
方法來解決這一問題。$watchCollection
在髒檢查的時候對對象進行淺比較,只會比較對象的第一層屬性。 -
儘量把 DOM 操作移到指令中
比如ng-show
和ng-hide
,我們經常通過這些指令來控制元素的顯示和隱藏,但這些指令的表達式值都會被 AngularJS 監聽,導致watcher
增加,而且這些值的變化通常也會引發 AngularJS 的 digest。我們應該儘可能的把這些邏輯移到指令的link
函數中。當然,這一點最後考慮。
3. 其他建議
-
使用
track by
提高ng-repeat
性能 -
禁用 debug 信息 我們看到使用 AngularJS 指令的元素上被添加了許多類,比如
ng-binding
、ng-scope
等,這些類除了調試沒有任何作用,1$compileProvider.debugInfoEnabled(false) -
耗時的計算考慮移到 web workers 執行
工具
怎麼測時間都花在哪了?如果我們擔心某個函數會很耗時,可以簡單的把console.time()
和 console.timeEnd()
放在代碼的前後來測試代碼的運行時間。
1 2 3
|
console.time('myTimer') // your code here console.timeEnd('myTimer')
|
這兩個函數可以幫助我們測試某一小段代碼的運行時間,如果要觀測整個應用的運行時間,就要使用下面這兩個工具了:
- AngularJS Batarang
- Chrome Timeline
具體怎麼使用這兩種工具就不細說了,尤其是 Chrome 開發者工具,每個前端工程師都應該學會用它進行性能調優。
Batarang 是一種很有意思的武器,蝙蝠形狀的迴旋刀,是各種電影動畫裏面蝙蝠俠的武器。
總結
從去年下半年開始,國內很多大公司都開始用 AngularJS 開發用戶後臺了,比如 upyun、 Ucloud(新版 UCloud 用戶後臺做的很不錯) 和 阿里雲。但 F12 查看源代碼就會發現,基本上都沒有做任何的性能優化。當然,現在電腦性能基本上都處於過剩的狀態,即使不做任何優化,只要頁面的 watcher
數量沒有多到 staticfile 那樣,基本上也不會有什麼問題,最多就是把頁面響應時間從幾百毫秒提升到幾十毫秒,1s 以內通常用戶都是還可以接受的。但作爲一個有追求的程序猿,對自己開發的產品有歸屬感,你還是可以明顯感受到幾百毫秒到幾十毫秒的巨大差異的。另外一個就是代碼規範,社區已經有許多最佳實踐了,借鑑最佳實踐來改善自己的代碼風格是另一個優化的方向。