提升 JavaScript 效能的技巧

 

筆者最近因為看到一些文章提到了這段2009年的影片,所以看了這段影片好幾次,雖然已經是三年前的資訊,但還是很實際的 JavaScript 知識。所以摘錄一些重點。講者 Nicholas C. Zakas 是 Professional JavaScript 一書的作者,也曾經(聽說離開了?)是 Yahoo! 的 Principal Front End Engineer。
一開始講者先說明了瀏覽器不會為你的 code 做什麼,唯一可以做出什麼改變的是你本人。並且這邊所講的技巧,並沒有要你不管在寫怎樣的 code  都要硬套進去,還是要進行適當的分析。重點應該要放在瞭解這些概念,並看看對於您的工作有沒有幫助。
主要分為四個領域來講,包含 Scope Management資料存取、迴圈DOM

Scope Management


 

講者一開始先以這一段據說很糟的程式碼(一個全域函式 setup)來當起始範例。要理解這個全域函式怎樣糟之前呢,我們先來看看 JavaScript 的 Scope Chain。
當你定義一個全域函式的時候,那根據 ECMAScript 規格,它就會有一個 [[Scope]]屬性,而這個 [[Scope]] 會指到一個 Scope Chain Table,這個 table 裡面存放一個指到 global variables 的 table。
之後,當一個函式被執行的時候,對應的 execution context 會被生成,而這個 execution context  會有一個屬於自己的 scope chain,這個 scope chain 會被用來作變數解析。這個 execution context 一開始先把函式的 [[scope]] 複製一份,之後再產生一個 activation object 裡面指到所有的 local variables table,並把這個 activation object 放在 scope chain 的一開始,所以當 setup 被執行的時候 scope chain 應該是長這樣:

 

之後當你在 function scope 裡面做任何變數的存取的時候,第一步就是先從位於 0 的 scope chain 開始找,如果沒找到就會再往下一個位置去找。這也是一般
大家理解的會先取用 local 的,之後再往上一層,最後一直都找不到的話,就會產生錯誤。這邊的重點就是 global 的變數永遠都會在 scope chain 的最後面那一層,所以盡可能地使用 local variables,因為這總是比 global variables快。這邊跟之前我們解讀 jQuery 原始碼裡面提到的將 window 轉換成 local 所提到的效能問題是一樣的。不過這邊可以想像的到由於 jQuery 內部應用了許多 closure 技巧,所以很多時候 global variables 可能已經被往後推了非常多層。jQuery 為了改善這一塊,特別在進入點的地方就做了處理。
講者還對不同瀏覽器作了實驗,不過因為是三年前的資料,所以 Chrome 跟 Firefox 都是很早期的版本。如果先不論 IE 的話,其實以當時的 Chrome / FireFox 在 scope 這邊的最佳化似乎已經做得很好了。難怪現在常有一派說法是說不用太管這個,除非你的站的流量大到跟 google 或 yahoo 一樣,不然這些效能跟你應該是沒什麼關係的。即使是最爛的 IE7,這邊是 20 萬個讀取也還花不到 0.2 秒。所以就參考看看吧,我覺得現在的話了解scope management的知識才是重點,不用硬改這個。

關於 scope management,還有一點很重要的是以前常聽到人家說不要用 with,而 scope 是其中的一個原因。當你用了 with 的時候其實是在 scope chain 裡面硬加了一個暫時的 scope 在最前面的地方,當離開 with scope 的時候,這個 with scope 物件就消失。所以在 with 的範圍內,所有原本的 local variables 的存取都變慢了。 JavaScript 之神 Douglas Crockford 還曾寫過一篇文章來要你不要使用 with statement。另外,try/catch 也一樣有這個問題。
在 closure 的部分,可以想像的是至少會有三個 scope chain,一個是 global,一個是 containing function 的 activation context,還有一個是最前面的 local。可以想見的是 closure 的使用也會影響資料存取的效能,因為存取階層變多的關係。
在 scope management 這邊,講者總結了幾點建議:
  1. 對那些常常會存取的變數,盡量把它放在 local
  2. 避免使用 with
  3. 小心使用 try / catch
  4. 沒有必要的話不要用 closure
  5. 不要忘記在宣告變數時要加上 var,不然你會不小心宣告太多全域變數
根據以上原則,範例函式應該可以這樣修正:

 

資料存取方式

有四種存取資料的方式分別是:
  1. 數值或字串  (literal value)
  2. 變數
  3. 物件屬性
  4. 陣列
在這四種方式裡面,literal 與區域變數的存取效率都很好,兩者不相上下。而物件跟陣列的存取相對於前者,效能就差很多。講者一樣針對不同瀏覽器作了實驗。
 一樣是三年前的資料,看起來現代瀏覽器對這部分的最佳化也都做得很好了。甚至連很糟的IE其實也只花了0.09秒而已。如果你不是太在乎 IE 上的表現的話,我覺得幾乎可以忽略這部分的影響 XD

除此之外,講者還特別提到物件屬性的深度也會對效能有影響。深度越深的話就自然得會越滿慢。所以在資料結構的設計上要小心。
在資料存取的建議是:
  • 如果有一個物件屬性或陣列元素會被用到超過一次,就用區域變數取代它。
  • 盡量減低物件或是陣列存取的深度。

迴圈

這一部分講者講了幾個技巧,但我覺得跟 JavaScript 的特性無關啊,只是一般的演算法改進。重點應該只在於不要使用 for … in 跟 for each。當然各個 JS framework 提供的 each 也是少用。尤其是每一次 iteration 都要執行一次函式的方式盡量少用。


DOM

在談 JavaScript 的效能問題,就不能不提到 DOM,講者在 present 的時候嘗試講了笑話,但現場沒有任何反應,有點冏 XDD。首先提到的是邪惡的 HTMLCollection,透過 document.getElementsByTagName之類的函式取得的 HTMLCollection 的存取都很慢。因為每一次的存取都會重新做一次 DOM query。所以要盡量避免在迴圈中存取 HTMLCollection。但這畢竟是不可能的,所以講者建議將 HTMLCollection 轉成陣列後再做處理。不過如果你用 jQuery 的話大概不用擔心這個問題,筆者前一篇解讀 jQuery 原始馬的文章有提到 jQuery  會把 selector query 出來的 collection 轉成陣列,因此大概不會有這個問題。
關於 DOM 的效能問題,還有一個比較麻煩的是 ReFlow,幾乎所有跟 DOM 物件的操作都會引發 ReFlow,新增或是移除 DOM 物件,或是改變 CSS 屬性,甚至是讀取 DOM 物件屬性,都有可能引發 ReFlow。要解決這個問題,必須利用 DocumentFragment,這是一個類似 document 的物件,但是並不在實際的 DOM Tree 裡面,因此在這個 fragment 上做操作不會引發 ReFlow,之後只要將這個 fragment add 到 DOM,所有的 fragment children 都會被加入到實際的 DOM Tree 中。在你其實並不懂 JavaScript 一文中亦有提及一個好的 JavaScript 開發者必須瞭解如何透過 DocumentFragment 來有效率的新增或移除 DOM Nodes。
另外要避免個別的 CSS 屬性修改,因為每改一次就會觸發一次 ReFlow,最好是將要修改的屬性包裝成一個 CSS Class,之後改變對應的 className,這樣會大量地縮減 ReFlow  的次數。
結論
總結來說,筆者認為除了 DOM 的部分之外,其他的再現在的瀏覽器上其實改善的空間有限。也許在 coding 上的習慣可以調整,但應該也是不必去把既有的 code 作對應的調整。調整還是需要建立在對應的 profiling 資訊上。而 DOM 的操作,目前大部分的 framework 可能會 cover 一部分,不過在 ReFlow 的控制上可能還是需要 coding 的人自己多多小心的。最後祝大家 happy coding 🙂


No Responses

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *