優化AngularJS:1200毫秒到35毫秒的蛻變

蔡蔡發表於2013-11-11

編者:基於的熱情,我們計劃將這裡說的專案開源,繼續閱讀或者是傳送emailcontact@scalyr.com,也可以來Hacker News討論。

Scalyr,最近我們著手重寫我們的web客戶端, Scalyr Logs是多用途監視和日誌分析工具,在我們專用的日誌資料庫執行大多數的query都控制在幾十毫秒,但是每一次的頁面響應都需要載入頁面,大概需要好幾秒才可以呈現給使用者。

單頁面設計架構承諾不會再拖後臺的強勁表現的後退,所以我們開始尋找合適的框架,一個名叫AngularJS的脫穎而出,遵循著“fail fast”的原則,我們開始了挑戰之旅:log 檢視的重寫。

測試一個應用框架確實是個嚴峻的挑戰,當使用者點選日誌中任何一個單詞,我們就要搜尋出相關資訊,而頁面上可以點選的元素又不計其數;我們想讓日誌的分頁功能也瞬間得到反饋。我們其實已經預先獲取到了下一頁面的日誌資料,所以使用者介面的更新就成為了瓶頸,如果拿 AngularJS直接實現日誌檢視的換頁功能需要1.2秒,但是如果仔細優化一下的話就可以降到35毫秒。這些優化被證明在應用的其他部分也是適用的,並且對AngularJS適應性也很好。但我們必須打破一些規則來實現我們的想法,稍後討論。

log-view

一個Github更新的日誌demo

An AngularJS log viewer

本質上,日誌檢視就是一個日誌訊息的列表,每個字都可以點選。所以把Angular的指令加到DOM元素中,簡單實現如下:

在單頁面應用中有個數千個tokens是很正常的,在早期的測試中,我們發現進入日誌的下一頁會花費好幾秒來執行JavaScript。更糟的是,不相關的操作(比如點選導航下拉框)延遲也不輕,AngularJS的大神說最好把資料元素繫結的數量控制在200以下。對於一個單詞就是一個元素的我們來說,早已遠超這個數。

分析

ChromeJavaScript profiler工具,我們可以快速定位兩個拖延點。首先,每次更新要花大量時間在DOM元素的建立和銷燬上,如果新的view有不同的行數,或者任何一行有不同數量單詞,Angularng-repeat指令就會建立或者銷燬DOM元素,這個代價太大了。

其次,每一個單詞都有自己的change watcherAngularJSwatch這些單詞,一旦滑鼠點選就會觸發,這個是影響不相關操作(下拉選單導航)延遲的罪魁禍首。

優化#1:快取DOM elements

我們建立了一個ng-repeat指令的變體,在我們的版本中,如果繫結資料的數量減少了,超出的DOM元素會隱藏而不是銷燬,如果元素的數量過會兒有增加了,我們會重用這些快取的元素。

優化#2Aggregate watchers

用來呼叫change watchers的所有時間大部分都浪費了,在我們的應用中,特定單詞上的資料繫結都是永遠不會改變的除非整個日誌訊息變化,為了達成這一點,我們建立了一個指令”hides“隱藏掉了子元素的change watchers,只有等特定父元素表示式修改的時候才會呼叫他們。就這樣,我們避免了在每一次滑鼠點選或者其他微小的修改而導致的全盤change watchers(為了實現這個想法,我們稍微修改了AngularJS的抽象層,我們稍後再細說)。

優化#3:推遲元素建立

前面說了,我們為日誌裡的每一個單詞單獨建立了DOM,我們可以利用每一行的單個DOM元素得到相同的視覺呈現;其他元素都是為響應滑鼠點操作而建立的,因此,我們決定推遲這部分建立,只有當滑鼠移動到某行的時候我們再建立他。

為了實現這個,我們為每一行建立了兩個版本,一個就是簡單的文字元素來顯示完整的日誌資訊,另外一行就是個佔位符,用來顯示最終為每一個單詞填充後的效果。這個佔位符開始是隱藏的,當滑鼠移動到那一行的時候才會顯示,而簡單文字那一行這個時候就隱藏掉。下面會講到,顯示佔位符是如何填充單詞元素的。

優化#4:避開對隱藏元素的監視

我們建立了另外一個指令,用來阻止對隱藏元素的監視,這個指令支援優化#1,相較於原資料,我們多了更多的隱藏DOM節點,所以必須消除對多出來的DOM節點的監視。這也支援優化#3,讓推遲單詞節點的建立更加容易。因為直到這行資料的tokenized版本出現我們才會建立他 。

下面的程式碼就是所有的優化後的樣子,我們自定義的指令是粗體顯示。

Sly-repeat ng-repeat的變體,用來隱藏多出來的DOM元素而不是銷燬他們,sly-evaluate-only-when阻止內部change watchers除非“logLines”變數修改,sly-prevent-evaluation-when-hidden主要負責當滑鼠移動到指定行的上面的時候,隱藏的div才顯示。

這裡展示出了AngularJS對於封裝和分離的控制力,我們做了複雜的優化但是並沒有影響模板的結構(這裡展示的程式碼並不是真正產品裡的程式碼,但是他展示了所有的要點)。

結果

我們來看一下效果,我們新增了一些程式碼來衡量,從滑鼠點選開始,一直到Angular’s $digest迴圈結束(意味著更新DOM結束)。 

我們衡量點選”下一頁“按鈕的效能是通過Tomcat日誌,環境用的是MacBook Pro上的Chrome,結果見下表(每個資料都是10次測試的平均值):

資料已經快取 從伺服器獲取資料
簡單實現 1190 ms 1300 ms
優化後 35 ms 201 ms

這些資料不包括瀏覽器用在DOM佈局和重繪(JavaScript執行完成後)的時間,每次大概30毫秒。儘管如此,效果也顯而易見;下一頁的響應時間從1200毫秒驟降至35毫秒(如果算上渲染是65毫秒)。

“從伺服器獲取資料”裡的資料包括了我們使用AJAX從後端獲取log資料的時間。這個跟點選下一頁按鈕不同,因為我們預取下一頁的log資料,但是或許適用於其他的UI響應。即使這樣,優化後的程式也可以做到實時更新。

總結

這些程式碼正式運轉倆月了,結果相當讓人滿意。想看實際效果的請移步scalyr.com點選頁面最下面的“Try The Demo”連結,然後點選”Log View”,試一下下一頁按鈕。很快是吧。你一定不敢相信這是從一個執行著的server上看到實時資料。

實現上述優化確實花了不少時間。看起來是我們建立了一個自定義指令用來生成所有的log檢視,繞開ng-repeat。然後,這些都是有違AngularJS精神的,還要承擔程式碼維護的成本,測試成本以及其他因素。因為log檢視是我們對AngularJS做的測試工程,我們需要驗證這個解決方案的可行性。而且,我們建立的這些新指令已經用到了應用的別的部分了。

我們盡最大努力踐行Angular精神,但是我們必須對AngularJS的抽象層做出修改才可以實現這些優化。我們僭越了Scope$watch來攔截watcher註冊,然後必須倍加小心的操作Scope的例項變數來控制watcher$digest過程裡的執行。

下一次

這篇文章討論了一系列技術點,我們是效率最大化的忠實擁躉,前面介紹的優化只是我們用的一些小妙招而已,在後續的文章裡我們會繼續討論如何減少網路請求,網路延遲,伺服器執行時間等等。當然我們會繼續討論在開發自己的應用時是如何構建AngularJS的。如果你對這些感興趣,請留下你們寶貴的意見。

強力插入廣告

Scalyr,我們一直致力於通過好的技術來提高DevOps的體驗。都讀到這了,不妨來scalyr.com看看我們到底進展到哪裡了!

相關文章