AngularJS 應用優化指南

Batarang

前兩天因爲想用國內的 JS CDN,訪問到 staticfile(七牛提供的一個免費 CDN 服務),導致我的 Chrome 直接卡死了兩次,頁面關也關不掉,只能退出重啓。後來換到 Chrome Canary 才勉強可以訪問,用谷歌開發者工具一看,發現前端是用的 AngularJS ,大量的 ng-repeat,5萬多 watcher,沒有做任何性能優化,怎麼可能不卡。

Profile

今年以來一直在陸陸續續的優化公司基於 AngularJS 開發的應用,也積累了一些經驗,正好總結一下 AngularJS webapp 的性能優化指南。

1. 減少 watcher

我們知道 AngularJS 通過 髒檢查(digest cicle)來更新視圖,保持數據和視圖的同步,髒檢查的效率是和 watcher 的多少成正相關的,一般來說超過 2000 後就會明顯感覺到變慢,所以提高 AngularJS 性能的關鍵就是減少 watcher 的數量。

首先,我們要知道的是,什麼會產生 watcher ?

  • $scope.$watch
  • { { stuff } } 類模板語法
  • 大多數指令 (比如 ng-showng-if)
  • Scope 變量 scope: { bar: '='}
  • 過濾器 { { value | myFilter } }
  • ng-repeat 指令

上面這些情況都會產生 watcher,那麼問題來了,怎麼減少 watcher 呢?

  1. 使用單次綁定語法 { {::} }
    AngularJS 從1.3版本開始支持單向綁定語法 :: ,它可以明確的告訴 AngularJS 哪些綁定獲取到數據以後就不用關注了,這可以極大的減少 watcher 的數量,尤其是在 ng-repeat 內使用。
  2. 避免在 ng-repeat 中使用 filter
    可以先把數據過濾後再傳給 ng-repeat,這樣就能避免因爲過濾器產生的 watcher 了。
  3. 儘可能的使用 ng-if 而不是 ng-show
    ng-if 可以從 Dom 中移除元素,觸發 element.$destory(),刪除 ng-if 內的元素的 watcherng-show 仍然會 render 元素,只是設置樣式爲 display:none。 但是如果元素需要經常變動隱藏還是顯示,那麼使用 ng-show 可能會更好,ng-show 會緩存 Dom,不需要重複解析。
  4. 使用 $watchCollection 替代 $watch

2. 減少 digest 次數和範圍

減少 watcher 是從根本上解決問題,如果 watcher 的優化已經做到極致了,那麼這時候就應該換一種思路了。導致 AngularJS App 變慢的原因是 watcher 太多導致 digest 變慢,watcher 已經無法優化了,那麼就應該考慮從 digest 的下手了。

同樣,首先要知道的是,什麼情況下會觸發 AngularJS 髒檢查?

  • 用戶行爲(ng-clickng-changeng-model,etc)
  • $http 接口響應
  • $q promises resolved
  • 使用 $timeout 和 $interval
  • 你手動調用 $scope.$apply 或 $scope.$digest

優化主要從兩個方向進行,減少髒檢查的次數 和 縮小髒檢查的範圍

  1. 儘量使用 $scope.$digest 替代 $scope.$apply
    $scope.$digest 從當前 scope 向下進行髒檢查,而 $scope.$apply 會觸發整個應用自頂向下進行髒檢查,所以,使用 $scope.$digest 一般能大大的縮小髒檢查的範圍。

  2. 使用 $applyAsync 合併 http 請求
    通常在 App 啓動的時候,會同時發起好幾個 http 請求,來獲取用戶權限或賬戶信息之類的信息,每次接口返回值的時候,都會觸發 AngularJS 的髒檢查。這時候,如果可以等到這幾個接口都返回以後,再觸發髒檢查,就能將髒檢查的數量由幾次減小到1次了。$httpProvider 的 useApplyAsync 方法就是來解決這個問題,它通過 $rootScope.$applyAsync 把大約同一時間(10ms左右)收到的返回值組合到一起處理。applyAsync的實現機制其實就是事件循環,通過 setTimeout(fn,0) 來延遲執行函數,可以參考我寫的《深入學習 Zone》瞭解更多。

    1
    2
    3
    app.config(function ($httpProvider) {
    $httpProvider.useApplyAsync(true)
    })
  3. ng-model 防抖動( Debounce )
    搜索框通常會監聽用戶的 keyup 事件來進行實時匹配推薦,如果每次用戶按下按鍵都調用接口,會出現多次連續的調用接口,導致連續的觸發 AngularJS 髒檢查,這樣很容易造成頁面卡頓。這時,可以通過 ng-model 的 debounce 參數來限制髒檢查的間隔,比如 ng-model-options="{ debounce: 250 },限制每 250ms 內只進行一次髒檢查。

  4. 使用 $watchCollection 替代 $watch 的第三個參數
    $watch 只會比較對象引用是否相同,如果新值和原始值指向同一個索引,那麼 $digest 時就不會觸發回調函數。如果要監視對象的每個屬性,我們可以給 $watch 傳入第三個參數 true,這樣 AngularJS 就會對對象進行深比較(使用 angular.equals),遍歷對象的每個值判斷是否發生了變化。但如果對象比較複雜,這樣做就會帶來很大的性能損耗。所以,AngularJS 提供了 $watchCollection 方法來解決這一問題。$watchCollection 在髒檢查的時候對對象進行淺比較,只會比較對象的第一層屬性。

  5. 儘量把 DOM 操作移到指令中
    比如 ng-show 和 ng-hide,我們經常通過這些指令來控制元素的顯示和隱藏,但這些指令的表達式值都會被 AngularJS 監聽,導致 watcher 增加,而且這些值的變化通常也會引發 AngularJS 的 digest。我們應該儘可能的把這些邏輯移到指令的 link 函數中。當然,這一點最後考慮。

3. 其他建議

  1. 使用 track by 提高 ng-repeat性能

  2. 禁用 debug 信息 我們看到使用 AngularJS 指令的元素上被添加了許多類,比如 ng-bindingng-scope等,這些類除了調試沒有任何作用,

    1
    $compileProvider.debugInfoEnabled(false)
  3. 耗時的計算考慮移到 web workers 執行

工具

怎麼測時間都花在哪了?如果我們擔心某個函數會很耗時,可以簡單的把console.time() 和 console.timeEnd() 放在代碼的前後來測試代碼的運行時間。

1
2
3
console.time('myTimer')
// your code here
console.timeEnd('myTimer')

這兩個函數可以幫助我們測試某一小段代碼的運行時間,如果要觀測整個應用的運行時間,就要使用下面這兩個工具了:

具體怎麼使用這兩種工具就不細說了,尤其是 Chrome 開發者工具,每個前端工程師都應該學會用它進行性能調優。

Batarang 是一種很有意思的武器,蝙蝠形狀的迴旋刀,是各種電影動畫裏面蝙蝠俠的武器。

總結

從去年下半年開始,國內很多大公司都開始用 AngularJS 開發用戶後臺了,比如 upyun、 Ucloud(新版 UCloud 用戶後臺做的很不錯) 和 阿里雲。但 F12 查看源代碼就會發現,基本上都沒有做任何的性能優化。當然,現在電腦性能基本上都處於過剩的狀態,即使不做任何優化,只要頁面的 watcher 數量沒有多到 staticfile 那樣,基本上也不會有什麼問題,最多就是把頁面響應時間從幾百毫秒提升到幾十毫秒,1s 以內通常用戶都是還可以接受的。但作爲一個有追求的程序猿,對自己開發的產品有歸屬感,你還是可以明顯感受到幾百毫秒到幾十毫秒的巨大差異的。另外一個就是代碼規範,社區已經有許多最佳實踐了,借鑑最佳實踐來改善自己的代碼風格是另一個優化的方向。

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章