提高web應用性能之JavaScript性能調優

JavaScript 語言由於它的單線程和解釋執行的兩個特點,決定了它本身有很多地方有性能問題,所以可改進的地方有不少。

 

eval 的問題:

 

比較下述代碼:

清單 1. eval 的問題

 

var reference = {}, props = “p1”; eval(“reference.” + props + “=5”) var reference = {}, props = “p1”; reference[props] = 5

有“eval”的代碼比沒有“eval”的代碼要慢上 100 倍以上。

 

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

 

Function 的用法:

 

比較下述代碼:

清單 2. function 的用法

 

var func1 = new Function(“return arguments[0] + arguments[1]”); func1(10, 20); var func2 = function(){ return arguments[0] + arguments[1] }; func2(10, 20);

這裏類似之前提到的“eval”方法,這裏“func1”的效率會比“func2”的效率差很多,所以推薦使用第二種方式。

 

函數的作用域鏈(scope chain):

 

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

 

局部變量放入層級 1(淺),全局變量放入層級 2(深)。如果進入“with”或“try – catch”代碼塊,則會增加新的層級,即將“with”或“catch”裏的變量放入最淺層(層 1),並將之前的層級依次加深。

 

參考如下代碼:

清單 3. 函數作用域鏈

 

var myObj = … .. … .. function process(){ var images = document.getElementsByTagName("img"), widget = document.getElementsByTagName("input"), combination = []; for(var i = 0; i < images.length; i++){ combination.push(combine(images[i], widget[2*i])); } myObj.container.property1 = combination[0]; myObj.container.property2 = combination[combination.length-1]; }

這裏我們可以看到,“images”,“widget”,“combination”屬於局部變量,在層 1。“document”,“myObj”屬於全局變量,在層 2。

 

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

清單 4. 使用局部變量

 

var myObj = … .. … .. function process(){ var doc = document; var images = doc.getElementsByTagName("img"), widget = doc.getElementsByTagName("input"), combination = []; for(var i = 0; i < images.length; i++){ combination.push(combine(images[i], widget[2*i])); } myObj.container.property1 = combination[0]; myObj.container.property2 = combination[combination.length-1]; }

我們用局部變量“doc”取代全局變量“document”,這樣可以改進性能,尤其是對於大量使用全局變量的函數裏面。

 

再看如下代碼:

清單 5. 慎用 with

 

var myObj = … .. … .. function process(){ var doc = document; var images = doc.getElementsByTagName("img"), widget = doc.getElementsByTagName("input"), combination = []; for(var i = 0; i < images.length; i++){ combination.push(combine(images[i], widget[2*i])); } with (myObj.container) { property1 = combination[0]; property2 = combination[combination.length-1]; } }

加上“with”關鍵字,我們讓代碼更加簡潔清晰了,但是這樣做性能會受影響。正如之前說的,當我們進入“with”代碼塊時,“combination”便從原來的層 1 變到了層 2,這樣,效率會大打折扣。所以比較一下,還是使用原來的代碼:

清單 6. 改進 with

 

var myObj = … .. … .. function process(){ var doc = document; var images = doc.getElementsByTagName("img"), widget = doc.getElementsByTagName("input"), combination = []; for(var i = 0; i < images.length; i++){ combination.push(combine(images[i], widget[2*i])); } myObj.container.property1 = combination[0]; myObj.container.property2 = combination[combination.length-1]; }

但是這樣並不是最好的方式,JavaScript 有個特點,對於 object 對象來說,其屬性訪問層級越深,效率越低,比如這裏的“myObj”已經訪問到了第 3 層,我們可以這樣改進一下:

清單 7. 縮小對象訪問層級

 

var myObj = … .. … .. function process(){ var doc = document; var images = doc.getElementsByTagName("img"), widget = doc.getElementsByTagName("input"), combination = []; for(var i = 0; i < images.length; i++){ combination.push(combine(images[i], widget[2*i])); } var ctn = myObj.container; ctn.property1 = combination[0]; ctn.property2 = combination[combination.length-1]; }

我們用局部變量來代替“myObj”的第 2 層的“container”對象。如果有大量的這種對對象深層屬性的訪問,可以參照以上方式提高性能。

 

字符串(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. 隱式類型轉換

 

var str = “12345678”, arr = []; for(var i = 0; i <= s.length; i++){ arr.push( str.charAt(i)); }

這裏我們在每個循環時都會調用字符串的“charAt”方法,但是由於我們是將常量“12345678”賦值給“str”,所以“str” 這裏事實上並不是一個字符串對象,當它每次調用“charAt”函數時,都會臨時構造值爲“12345678”的字符串對象,然後調用“charAt”方 法,最後再釋放這個字符串臨時對象。我們可以做一些改進:

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

 

var str = new Stirng(“12345678”), arr = []; for(var i = 0; i <= s.length; i++){ arr.push( str.charAt(i)); }

這樣一來,變量“str”作爲一個字符串對象,就不會有這種隱式類型轉換的過程了,這樣一來,效率會顯著提高。

 

字符串匹配

 

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

 

是否能夠通過字符串對象本身支持的基本方法解決問題。

是否可以通過“substring”來縮小需要用正則表達式的範圍。

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

 

關於正則表達式對象,還有一點需要注意,參考如下代碼:

清單 13. 正則表達式

 

for(var i = 0; i <= str_array.length; i++){ if(str_array[i].match(/^s*extras/)){ …………………… } }

這裏,我們往“match”方法傳入“/^s*extras/”是會影響效率的,它會構建臨時值爲“/^s*extras/”的正則表達式對象,執行“match”方法,然後銷燬臨時的正則表達式對象。我們可以這樣做:

清單 14. 利用變量

 

var sExpr = /^s*extras/; for(var i = 0; i <= str_array.length; i++){ if(str_array[i].match(sExpr)){ …………………… } }

這樣就不會有臨時對象了。

 

setTimeout 和 setInterval

 

“setTimeout”和“setInterval”這兩個函數可以接受字符串變量,但是會帶來和之前談到的“eval”類似的性能問題,所以建議還是直接傳入函數對象本身。

 

利用提前退出

 

參考如下兩段代碼:

清單 15. 利用提前退出

 

// 代碼 1 var name = … .; var source = …… ; if(source.match(/ …… /)){ …………………………… } // 代碼 2 var name = … .; var source = …… ; if(name.indexOf( … ) &&source.match(/ …… /)){ …………………………… }

代碼 2 多了一個對“name.indexOf( … )”的判斷,這使得程序每次走到這一段時會先執行“indexOf”的判斷,再執行後面的“match”,在“indexOf”比“match”效率高很 多的前提下,這樣做會減少“match”的執行次數,從而一定程度的提高效率。

 

DOM 操作性能調優

 

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

 

Repaint 和 Reflow

 

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

 

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

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

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

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

 

瀏覽器窗口的變化

DOM 節點的添加刪除操作

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

減少 Reflow

 

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

 

參考如下代碼:

清單 16. reflow 介紹

 

var pDiv = document.createElement(“div”); document.body.appendChild(pDiv);----- reflow var cDiv1 = document.createElement(“div”); var cDiv2 = document.createElement(“div”); pDiv.appendChild(cDiv1);----- reflow pDiv.appendChild(cDiv2);----- reflow

這是我們經常接觸的代碼了,但是這段代碼會產生 3 次 reflow。再看如下代碼:

清單 17. 減少 reflow

 

var pDiv = document.createElement(“div”); var cDiv1 = document.createElement(“div”); var cDiv2 = document.createElement(“div”); pDiv.appendChild(cDiv1); pDiv.appendChild(cDiv2); document.body.appendChild(pDiv);----- reflow

這裏便只有一次 reflow,所以我們推薦這種 DOM 節點操作的方式。

 

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

清單 18. 利用 display 減少 reflow

 

var pDiv = document.getElementById(“parent”); pDiv.style.display = “none”----- reflow pDiv.appendChild(cDiv1); pDiv.appendChild(cDiv2); pDiv.appendChild(cDiv3); pDiv.appendChild(cDiv4); pDiv.appendChild(cDiv5); pDiv.style.width = “100px”; pDiv.style.height = “100px”; pDiv.style.display = “block”----- reflow

先隱藏 pDiv,再顯示,這樣,隱藏和顯示之間的操作便不會產生任何的 Reflow,提高了效率。

 

特殊測量屬性和方法

 

DOM 元素裏面有一些特殊的測量屬性的訪問和方法的調用,也會觸發 Reflow,比較典型的就是“offsetWidth”屬性和“getComputedStyle”方法。

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

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

 

這些測量屬性和方法大致有這些:

 

offsetLeft

offsetTop

offsetHeight

offsetWidth

scrollTop/Left/Width/Height

clientTop/Left/Width/Height

getComputedStyle()

currentStyle(in IE))

這些屬性和方法的訪問和調用,都會觸發 Reflow 的產生,我們應該儘量減少對這些屬性和方法的訪問和調用,參考如下代碼:

清單 19. 特殊測量屬性

 

var pe = document.getElementById(“pos_element”); var result = document.getElementById(“result_element”); var pOffsetWidth = pe.offsetWidth; result.children[0].style.width = pOffsetWidth; result.children[1].style.width = pOffsetWidth; result.children[2].style.width = pOffsetWidth; …………其他修改…………

這裏我們可以用臨時變量將“offsetWidth”的值緩存起來,這樣就不用每次訪問“offsetWidth”屬性。這種方式在循環裏面非常適用,可以極大地提高性能。

 

樣式相關

 

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

清單 20. 樣式相關

 

var sElement = document.getElementById(“pos_element”); sElement.style.border = ‘ 1px solid red ’ sElement.style.backgroundColor = ‘ silver ’ sElement.style.padding = ‘ 2px 3px ’ sElement.style.marginLeft = ‘ 5px ’

但是可以看到,這裏的每一個樣式的改變,都會產生 Reflow。需要減少這種情況的發生,我們可以這樣做:

 

解決方案 1:

清單 21. className 解決方案

 

.class1 { border: ‘ 1px solid red ’ background-color: ‘ silver ’ padding: ‘ 2px 3px ’ margin-left: ‘ 5px ’ } document.getElementById(“pos_element”).className = ‘class1’ ;

用 class 替代 style,可以將原有的所有 Reflow 或 Repaint 的次數都縮減到一個。

 

解決方案 2:

清單 22. cssText 解決方案

 

var sElement = document.getElementById(“pos_element”); var newStyle = ‘ border: 1px solid red; ’ + ‘ background-color: silver; ’ + ‘ padding: 2px 3px; ’ + “margin-left: 5px;” sElement.style.cssText += newStyle;

一次性設置所有樣式,也是減少 Reflow 提高性能的方法。

 

XPath

 

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

清單 23. XPath 解決方案

 

if(document.evaluate){ var tblHeaders = document.evaluate(“//body/div/table//th”); var result = tblHeaders.iterateNext(); while(result) { result.style.border = “1px dotted blue”; result ……………… result = xpathResult.iterateNext(); } } else{ //getElementsByTagName() …… // 處理瀏覽器不支持 XPath 的情況 ……………………………… }

瀏覽器 XPath 的搜索引擎會優化搜索效率,大大縮短結果返回時間。

 

HTMLCollection 對象

 

這是一類特殊的對象,它們有點像數組,但不完全是數組。下述方法的返回值一般都是 HTMLCollection 對象:

 

document.images, document.forms

getElementsByTagName()

getElementsByClassName()

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

 

Length 屬性

具體的某個成員

所以,HTMLCollection 對象對這些屬性和成員的訪問,比起數組來要慢很多。當然也有例外,Opera 和 Safari 對這種情況就處理的很好,不會有太大性能問題。

 

hxyy2013.b2b168.com

http://www.wenbing.com/kmhx

參考如下代碼:

清單 24. HTMLConnection 對象

 

var items = [“test1”, “test2”, “test3”, ……………… ]; for(var i = 0; i < items.length; i++){ ……………………………… } var items = document.getElementsByTagName(“div”); for(var i = 0; i < items.length; i++){ …………………………………… . }

上述兩端代碼,下面的效率比起上面一段要慢很多,因爲每一個循環都會有“items.length”的觸發,也就會導致“document.getElementsByTagName(..)”方法的再次調用,這便是效率便會大幅度下降的原因。我們可以這樣解決:

清單 25. HTMLConnection 對象解決方案

 

var items = document.getElementsByTagName(“div”); var len = items.length for(var i = 0; i < len; i++){ …………………………………… . }

這樣一來,效率基本與普通數組一樣。

 

動態創建 script 標籤

 

加載並執行一段 JavaScript 腳本是需要一定時間的,在我們的程序中,有時候有些 JavaScript 腳本被加載後基本沒有被使用過 (比如:腳本里的函數從來沒有被調用等等)。加載這些腳本只會佔用 CPU 時間和增加內存消耗,降低 Web 應用的性能。所以推薦動態的加載 JavaScript 腳本文件,尤其是那些內容較多,消耗資源較大的腳本文件。

清單 26. 創建 script 標籤

 

if(needXHR){ document.write(“<script type= ’ test/JavaScript ’ src= 'dojo_xhr.js' >”); } if(dojo.isIE){ document.write(“<script type= ’ test/JavaScript ’ src= 'vml.js' >”); }


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