1. 程式人生 > >技術進階:通過來JavaScript 效能調優提高 Web 應用效能

技術進階:通過來JavaScript 效能調優提高 Web 應用效能

前言

現在的網際網路應用中,在Web 開發中經常會遇到效能的問題,尤其是針對當今的 Web2.0 +應用。JavaScript 是當今使用最為廣泛的 Web 開發語言,Web 應用的效能問題很大一部分都是由程式設計師寫的 JavaScript 指令碼效能不佳所造成的,裡面包括了 JavaScript 語言本身的效能問題,以及其與 DOM 互動時的效能問題。本文主要來探討一下如何儘可能多的避免這類問題,從而最大限度的提高 Web 應用的效能。
1.JavaScript 效能調優
JavaScript 語言由於它的單執行緒和解釋執行的兩個特點,決定了它本身有很多地方有效能問題,所以可改進的地方有不少。

1.1 eval 的問題:

比較下述程式碼:

清單 1. eval 的問題

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
有"eval"的程式碼比沒有"eval"的程式碼要慢上 100 倍以上。

主要原因是:JavaScript 程式碼在執行前會進行類似"預編譯"的操作:首先會建立一個當前執行環境下的活動物件,並將那些用 var 申明的變數設定為活動物件的屬性,但是此時這些變數的賦值都是 undefined,並將那些以 function 定義的函式也新增為活動物件的屬性,而且它們的值正是函式的定義。但是,如果你使用了"eval",則"eval"中的程式碼(實際上為字串)無法預先識別其上下文,無法被提前解析和優化,即無法進行預編譯的操作。所以,其效能也會大幅度降低。

1.2 Function 的用法

比較下述程式碼:

清單 2. function 的用法

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
這裡類似之前提到的"eval"方法,這裡"func1"的效率會比"func2"的效率差很多,所以推薦使用第二種方式。

1.3函式的作用域鏈(scope chain)

JavaScript 程式碼解釋執行,在進入函式內部時,它會預先分析當前的變數,並將這些變數歸入不同的層級(level),一般情況下:

區域性變數放入層級 1(淺),全域性變數放入層級 2(深)。如果進入"with"或"try – catch"程式碼塊,則會增加新的層級,即將"with"或"catch"裡的變數放入最淺層(層 1),並將之前的層級依次加深。

參考如下程式碼:

清單 3. 函式作用域鏈

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
這裡我們可以看到,“images”,“widget”,"combination"屬於區域性變數,在層 1。“document”,"myObj"屬於全域性變數,在層 2。

變數所在的層越淺,訪問(讀取或修改)速度越快,層越深,訪問速度越慢。所以這裡對"images",“widget”,“combination"的訪問速度比"document”,"myObj"要快一些。所以推薦儘量使用區域性變數,可見如下程式碼:

清單 4. 使用區域性變數

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
我們用區域性變數"doc"取代全域性變數"document",這樣可以改進效能,尤其是對於大量使用全域性變數的函式裡面。

再看如下程式碼:

清單 5. 慎用 with

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
加上"with"關鍵字,我們讓程式碼更加簡潔清晰了,但是這樣做效能會受影響。正如之前說的,當我們進入"with"程式碼塊時,"combination"便從原來的層 1 變到了層 2,這樣,效率會大打折扣。所以比較一下,還是使用原來的程式碼:

清單 6. 改進 with

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
但是這樣並不是最好的方式,JavaScript 有個特點,對於 object 物件來說,其屬性訪問層級越深,效率越低,比如這裡的"myObj"已經訪問到了第 3 層,我們可以這樣改進一下:

清單 7. 縮小物件訪問層級

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
我們用區域性變數來代替"myObj"的第 2 層的"container"物件。如果有大量的這種對物件深層屬性的訪問,可以參照以上方式提高效能。

1.4字串(String)相關

字串拼接

經常看到這樣的程式碼:

清單 8. 字串簡單拼接

str += “str1” + “str2”
這是我們拼接字串常用的方式,但是這種方式會有一些臨時變數的建立和銷燬,影響效能,所以推薦使用如下方式拼接:

清單 9. 字串陣列方式拼接

var str_array = [];
str_array.push(“str1”);
str_array.push(“str2”);
str = str_array.join("");
這裡我們利用陣列(array)的"join"方法實現字串的拼接,尤其是程式的老版本的 Internet Explore(IE6)上執行時,會有非常明顯的效能上的改進。

當然,最新的瀏覽器(如火狐 Firefox3+,IE8+ 等等)對字串的拼接做了優化,我們也可以這樣寫:

清單 10. 字串快速拼接

str +=“str1”
str +=“str2”
新的瀏覽器對"+=“做了優化,效能略快於陣列的"join"方法。在不久的將來更新版本瀏覽器可能對”+"也會做優化,所以那時我們可以直接寫:str += “str1” + “str2”。

隱式型別轉換

參考如下程式碼:

清單 11. 隱式型別轉換

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
這裡我們在每個迴圈時都會呼叫字串的"charAt"方法,但是由於我們是將常量"12345678"賦值給"str",所以"str"這裡事實上並不是一個字串物件,當它每次呼叫"charAt"函式時,都會臨時構造值為"12345678"的字串物件,然後呼叫"charAt"方法,最後再釋放這個字串臨時物件。我們可以做一些改進:

清單 12. 避免隱式型別轉換

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
這樣一來,變數"str"作為一個字串物件,就不會有這種隱式型別轉換的過程了,這樣一來,效率會顯著提高。

字串匹配

JavaScript 有 RegExp 物件,支援對字串的正則表示式匹配。是一個很好的工具,但是它的效能並不是非常理想。相反,字串物件(String)本身的一些基本方法的效率是非常高的,比如"substring",“indexOf”,"charAt"等等,在我們需要用正則表示式匹配字串時,可以考慮一下:

1)是否能夠通過字串物件本身支援的基本方法解決問題。

2)是否可以通過"substring"來縮小需要用正則表示式的範圍。

這些方式都能夠有效的提高程式的效率。

關於正則表示式物件,還有一點需要注意,參考如下程式碼:

清單 13. 正則表示式

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
這裡,我們往"match"方法傳入"/s*extras/“是會影響效率的,它會構建臨時值為”/s*extras/"的正則表示式物件,執行"match"方法,然後銷燬臨時的正則表示式物件。我們可以這樣做:

清單 14. 利用變數

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
這樣就不會有臨時物件了。

setTimeout 和 setInterval

"setTimeout"和"setInterval"這兩個函式可以接受字串變數,但是會帶來與之前談到的"eval"類似的效能問題,所以建議還是直接傳入函式物件本身。

利用提前退出

參考如下兩段程式碼:

清單 15. 利用提前退出

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
程式碼 2 多了一個對"name.indexOf( … )“的判斷,這使得程式每次走到這一段時會先執行"indexOf"的判斷,再執行後面的"match”,在"indexOf"比"match"效率高很多的前提下,這樣做會減少"match"的執行次數,從而一定程度的提高效率。

  1. DOM 操作效能調優
    JavaScript 的開發離不開 DOM 的操作,所以對 DOM 操作的效能調優在 Web 開發中也是非常重要的。

2.1 Repaint 和 Reflow

Repaint 也叫 Redraw,它指的是一種不會影響當前 DOM 的結構和佈局的一種重繪動作。如下動作會產生 Repaint 動作:

不可見到可見(visibility 樣式屬性)

顏色或圖片變化(background, border-color, color 樣式屬性)

不改變頁面元素大小,形狀和位置,但改變其外觀的變化

Reflow 比起 Repaint 來講就是一種更加顯著的變化了。它主要發生在 DOM 樹被操作的時候,任何改變 DOM 的結構和佈局都會產生 Reflow。但一個元素的 Reflow 操作發生時,它的所有父元素和子元素都會放生 Reflow,最後 Reflow 必然會導致 Repaint 的產生。舉例說明,如下動作會產生 Repaint 動作:

瀏覽器視窗的變化

DOM 節點的新增刪除操作

一些改變頁面元素大小,形狀和位置的操作的觸發

2.2 減少 Reflow

通過 Reflow 和 Repaint 的介紹可知,每次 Reflow 比其 Repaint 會帶來更多的資源消耗,我們應該儘量減少 Reflow 的發生,或者將其轉化為只會觸發 Repaint 操作的程式碼。

參考如下程式碼:

清單 16. reflow 介紹

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
這是我們經常接觸的程式碼了,但是這段程式碼會產生 3 次 reflow。再看如下程式碼:

清單 17. 減少 reflow

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
這裡便只有一次 reflow,所以我們推薦這種 DOM 節點操作的方式。

關於上述較少 Reflow 操作的解決方案,還有一種可以參考的模式:

清單 18. 利用 display 減少 reflow

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
先隱藏 pDiv,再顯示,這樣,隱藏和顯示之間的操作便不會產生任何的 Reflow,提高了效率。

2.3特殊測量屬性和方法

DOM 元素裡面有一些特殊的測量屬性的訪問和方法的呼叫,也會觸發 Reflow,比較典型的就是"offsetWidth"屬性和"getComputedStyle"方法。

圖 1. 特殊測量屬性和方法

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
這些測量屬性和方法大致有這些:

· offsetLeft
· offsetTop
· offsetHeight
· offsetWidth
· scrollTop/Left/Width/Height
· clientTop/Left/Width/Height
· getComputedStyle()
· currentStyle(in IE))
這些屬性和方法的訪問和呼叫,都會觸發 Reflow 的產生,我們應該儘量減少對這些屬性和方法的訪問和呼叫,參考如下程式碼:

清單 19. 特殊測量屬性

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
這裡我們可以用臨時變數將"offsetWidth"的值快取起來,這樣就不用每次訪問"offsetWidth"屬性。這種方式在迴圈裡面非常適用,可以極大地提高效能。

2.4 樣式相關

我們肯定經常見到如下的程式碼:

清單 20. 樣式相關

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
但是可以看到,這裡的每一個樣式的改變,都會產生 Reflow。需要減少這種情況的發生,我們可以這樣做:

解決方案 1:

清單 21. className 解決方案

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
用 class 替代 style,可以將原有的所有 Reflow 或 Repaint 的次數都縮減到一個。

解決方案 2:

清單 22. cssText 解決方案

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
一次性設定所有樣式,也是減少 Reflow 提高效能的方法。

2.5 XPath

一個頁面上往往包含 1000 多頁面元素,在定位具體元素的時候,往往需要一定的時間。如果用 id 或 name 定位可能效率不會太慢,如果用元素的一些其他屬性(比如 className 等等)定位,可能效率有不理想了。有的可能只能通過遍歷所有元素(getElementsByTagName)然後過濾才能找到相應元素,這就更加低效了,這裡我們推薦使用 XPath 查詢元素,這是很多瀏覽器本身支援的功能。

清單 23. XPath 解決方案

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
瀏覽器 XPath 的搜尋引擎會優化搜尋效率,大大縮短結果返回時間。

2.6 HTMLCollection 物件

這是一類特殊的物件,它們有點像陣列,但不完全是陣列。下述方法的返回值一般都是 HTMLCollection 物件:

· document.images, document.forms
· getElementsByTagName()
· getElementsByClassName()
這些 HTMLCollection 物件並不是一個固定的值,而是一個動態的結果。它們是一些比較特殊的查詢的返回值,在如下情況下,它們會重新執行之前的查詢而得到新的返回值(查詢結果),雖然多數情況下會和前一次或幾次的返回值都一樣:

· Length 屬性
· 具體的某個成員
所以,HTMLCollection 物件對這些屬性和成員的訪問,比起陣列來要慢很多。當然也有例外,Opera 和 Safari 對這種情況就處理的很好,不會有太大效能問題。

參考如下程式碼:

清單 24. HTMLConnection 物件

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
上述兩端程式碼,下面的效率比起上面一段要慢很多,因為每一個迴圈都會有"items.length"的觸發,也就會導致"document.getElementsByTagName(…)"方法的再次呼叫,這便是效率便會大幅度下降的原因。我們可以這樣解決:

清單 25. HTMLConnection 物件解決方案

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
這樣一來,效率基本與普通陣列一樣。

2.7 動態建立 script 標籤

載入並執行一段 JavaScript 指令碼是需要一定時間的,在我們的程式中,有時候有些 JavaScript 指令碼被載入後基本沒有被使用過 (比如:腳本里的函式從來沒有被呼叫等等)。載入這些指令碼只會佔用 CPU 時間和增加記憶體消耗,降低 Web 應用的效能。所以推薦動態的載入 JavaScript 指令碼檔案,尤其是那些內容較多,消耗資源較大的指令碼檔案。

清單 26. 建立 script 標籤

技術進階:通過來JavaScript 效能調優提高 Web 應用效能
寫在最後
這篇文章介紹了 Web 開發中關於效能方面需要注意的一些小細節,從 JavaScript 本身著手,介紹了 JavaScript 中需要避免的一些函式的使用和程式設計規則,比如 eval 的弊端,function scope chain 以及 String 的用法等等,也分享了一些比較推薦的做法,並擴充套件到 JavaScript 對 DOM 操作的效能調優,比如利用 Repaint 和 Reflow 的機制,如何使用特殊測量屬性,樣式相關的效能調優以及 HTMLCollection 物件的原理和使用小技巧。這些小細節我們可以在開發過程中儘量注意一下,以儘可能多的提高我們 Web 應用的效能。