技術(shù)進(jìn)階:通過來JavaScript 性能調(diào)優(yōu)提高 Web 應(yīng)用性能
前言
現(xiàn)在的互聯(lián)網(wǎng)應(yīng)用中,在Web 開發(fā)中經(jīng)常會遇到性能的問題,尤其是針對當(dāng)今的 Web2.0 +應(yīng)用。JavaScript 是當(dāng)今使用最為廣泛的 Web 開發(fā)語言,Web 應(yīng)用的性能問題很大一部分都是由程序員寫的 JavaScript 腳本性能不佳所造成的,里面包括了 JavaScript 語言本身的性能問題,以及其與 DOM 交互時(shí)的性能問題。本文主要來探討一下如何盡可能多的避免這類問題,從而最大限度的提高 Web 應(yīng)用的性能。
1.JavaScript 性能調(diào)優(yōu)
JavaScript 語言由于它的單線程和解釋執(zhí)行的兩個(gè)特點(diǎn),決定了它本身有很多地方有性能問題,所以可改進(jìn)的地方有不少。
1.1 eval 的問題:
比較下述代碼:
清單 1. eval 的問題
有"eval"的代碼比沒有"eval"的代碼要慢上 100 倍以上。
主要原因是:JavaScript 代碼在執(zhí)行前會進(jìn)行類似"預(yù)編譯"的操作:首先會創(chuàng)建一個(gè)當(dāng)前執(zhí)行環(huán)境下的活動對象,并將那些用 var 申明的變量設(shè)置為活動對象的屬性,但是此時(shí)這些變量的賦值都是 undefined,并將那些以 function 定義的函數(shù)也添加為活動對象的屬性,而且它們的值正是函數(shù)的定義。但是,如果你使用了"eval",則"eval"中的代碼(實(shí)際上為字符串)無法預(yù)先識別其上下文,無法被提前解析和優(yōu)化,即無法進(jìn)行預(yù)編譯的操作。所以,其性能也會大幅度降低。
1.2 Function 的用法
比較下述代碼:
清單 2. function 的用法
這里類似之前提到的"eval"方法,這里"func1"的效率會比"func2"的效率差很多,所以推薦使用第二種方式。
1.3函數(shù)的作用域鏈(scope chain)
JavaScript 代碼解釋執(zhí)行,在進(jìn)入函數(shù)內(nèi)部時(shí),它會預(yù)先分析當(dāng)前的變量,并將這些變量歸入不同的層級(level),一般情況下:
局部變量放入層級 1(淺),全局變量放入層級 2(深)。如果進(jìn)入"with"或"try – catch"代碼塊,則會增加新的層級,即將"with"或"catch"里的變量放入最淺層(層 1),并將之前的層級依次加深。
參考如下代碼:
清單 3. 函數(shù)作用域鏈
這里我們可以看到,"images","widget","combination"屬于局部變量,在層 1。"document","myObj"屬于全局變量,在層 2。
變量所在的層越淺,訪問(讀取或修改)速度越快,層越深,訪問速度越慢。所以這里對"images","widget","combination"的訪問速度比"document","myObj"要快一些。所以推薦盡量使用局部變量,可見如下代碼:
清單 4. 使用局部變量
我們用局部變量"doc"取代全局變量"document",這樣可以改進(jìn)性能,尤其是對于大量使用全局變量的函數(shù)里面。
再看如下代碼:
清單 5. 慎用 with
加上"with"關(guān)鍵字,我們讓代碼更加簡潔清晰了,但是這樣做性能會受影響。正如之前說的,當(dāng)我們進(jìn)入"with"代碼塊時(shí),"combination"便從原來的層 1 變到了層 2,這樣,效率會大打折扣。所以比較一下,還是使用原來的代碼:
清單 6. 改進(jìn) with
但是這樣并不是最好的方式,JavaScript 有個(gè)特點(diǎn),對于 object 對象來說,其屬性訪問層級越深,效率越低,比如這里的"myObj"已經(jīng)訪問到了第 3 層,我們可以這樣改進(jìn)一下:
清單 7. 縮小對象訪問層級
我們用局部變量來代替"myObj"的第 2 層的"container"對象。如果有大量的這種對對象深層屬性的訪問,可以參照以上方式提高性能。
1.4字符串(String)相關(guān)
字符串拼接
經(jīng)常看到這樣的代碼:
清單 8. 字符串簡單拼接
str += "str1" + "str2"
這是我們拼接字符串常用的方式,但是這種方式會有一些臨時(shí)變量的創(chuàng)建和銷毀,影響性能,所以推薦使用如下方式拼接:
清單 9. 字符串?dāng)?shù)組方式拼接
var str_array = [];
str_array.push("str1");
str_array.push("str2");
str = str_array.join("");
這里我們利用數(shù)組(array)的"join"方法實(shí)現(xiàn)字符串的拼接,尤其是程序的老版本的 Internet Explore(IE6)上運(yùn)行時(shí),會有非常明顯的性能上的改進(jìn)。
當(dāng)然,最新的瀏覽器(如火狐 Firefox3+,IE8+ 等等)對字符串的拼接做了優(yōu)化,我們也可以這樣寫:
清單 10. 字符串快速拼接
str +="str1"
str +="str2"
新的瀏覽器對"+="做了優(yōu)化,性能略快于數(shù)組的"join"方法。在不久的將來更新版本瀏覽器可能對"+"也會做優(yōu)化,所以那時(shí)我們可以直接寫:str += "str1" + "str2"。
隱式類型轉(zhuǎn)換
參考如下代碼:
清單 11. 隱式類型轉(zhuǎn)換
這里我們在每個(gè)循環(huán)時(shí)都會調(diào)用字符串的"charAt"方法,但是由于我們是將常量"12345678"賦值給"str",所以"str"這里事實(shí)上并不是一個(gè)字符串對象,當(dāng)它每次調(diào)用"charAt"函數(shù)時(shí),都會臨時(shí)構(gòu)造值為"12345678"的字符串對象,然后調(diào)用"charAt"方法,最后再釋放這個(gè)字符串臨時(shí)對象。我們可以做一些改進(jìn):
清單 12. 避免隱式類型轉(zhuǎn)換
這樣一來,變量"str"作為一個(gè)字符串對象,就不會有這種隱式類型轉(zhuǎn)換的過程了,這樣一來,效率會顯著提高。
字符串匹配
JavaScript 有 RegExp 對象,支持對字符串的正則表達(dá)式匹配。是一個(gè)很好的工具,但是它的性能并不是非常理想。相反,字符串對象(String)本身的一些基本方法的效率是非常高的,比如"substring","indexOf","charAt"等等,在我們需要用正則表達(dá)式匹配字符串時(shí),可以考慮一下:
1)是否能夠通過字符串對象本身支持的基本方法解決問題。
2)是否可以通過"substring"來縮小需要用正則表達(dá)式的范圍。
這些方式都能夠有效的提高程序的效率。
關(guān)于正則表達(dá)式對象,還有一點(diǎn)需要注意,參考如下代碼:
清單 13. 正則表達(dá)式
這里,我們往"match"方法傳入"/^s*extras/"是會影響效率的,它會構(gòu)建臨時(shí)值為"/^s*extras/"的正則表達(dá)式對象,執(zhí)行"match"方法,然后銷毀臨時(shí)的正則表達(dá)式對象。我們可以這樣做:
清單 14. 利用變量
這樣就不會有臨時(shí)對象了。
setTimeout 和 setInterval
"setTimeout"和"setInterval"這兩個(gè)函數(shù)可以接受字符串變量,但是會帶來與之前談到的"eval"類似的性能問題,所以建議還是直接傳入函數(shù)對象本身。
利用提前退出
參考如下兩段代碼:
清單 15. 利用提前退出
代碼 2 多了一個(gè)對"name.indexOf( … )"的判斷,這使得程序每次走到這一段時(shí)會先執(zhí)行"indexOf"的判斷,再執(zhí)行后面的"match",在"indexOf"比"match"效率高很多的前提下,這樣做會減少"match"的執(zhí)行次數(shù),從而一定程度的提高效率。
2. DOM 操作性能調(diào)優(yōu)
JavaScript 的開發(fā)離不開 DOM 的操作,所以對 DOM 操作的性能調(diào)優(yōu)在 Web 開發(fā)中也是非常重要的。
2.1 Repaint 和 Reflow
Repaint 也叫 Redraw,它指的是一種不會影響當(dāng)前 DOM 的結(jié)構(gòu)和布局的一種重繪動作。如下動作會產(chǎn)生 Repaint 動作:
不可見到可見(visibility 樣式屬性)
顏色或圖片變化(background, border-color, color 樣式屬性)
不改變頁面元素大小,形狀和位置,但改變其外觀的變化
Reflow 比起 Repaint 來講就是一種更加顯著的變化了。它主要發(fā)生在 DOM 樹被操作的時(shí)候,任何改變 DOM 的結(jié)構(gòu)和布局都會產(chǎn)生 Reflow。但一個(gè)元素的 Reflow 操作發(fā)生時(shí),它的所有父元素和子元素都會放生 Reflow,最后 Reflow 必然會導(dǎo)致 Repaint 的產(chǎn)生。舉例說明,如下動作會產(chǎn)生 Repaint 動作:
瀏覽器窗口的變化
DOM 節(jié)點(diǎn)的添加刪除操作
一些改變頁面元素大小,形狀和位置的操作的觸發(fā)
2.2 減少 Reflow
通過 Reflow 和 Repaint 的介紹可知,每次 Reflow 比其 Repaint 會帶來更多的資源消耗,我們應(yīng)該盡量減少 Reflow 的發(fā)生,或者將其轉(zhuǎn)化為只會觸發(fā) Repaint 操作的代碼。
參考如下代碼:
清單 16. reflow 介紹
這是我們經(jīng)常接觸的代碼了,但是這段代碼會產(chǎn)生 3 次 reflow。再看如下代碼:
清單 17. 減少 reflow
這里便只有一次 reflow,所以我們推薦這種 DOM 節(jié)點(diǎn)操作的方式。
關(guān)于上述較少 Reflow 操作的解決方案,還有一種可以參考的模式:
清單 18. 利用 display 減少 reflow
先隱藏 pDiv,再顯示,這樣,隱藏和顯示之間的操作便不會產(chǎn)生任何的 Reflow,提高了效率。
2.3特殊測量屬性和方法
DOM 元素里面有一些特殊的測量屬性的訪問和方法的調(diào)用,也會觸發(fā) Reflow,比較典型的就是"offsetWidth"屬性和"getComputedStyle"方法。
圖 1. 特殊測量屬性和方法
這些測量屬性和方法大致有這些:
· offsetLeft
· offsetTop
· offsetHeight
· offsetWidth
· scrollTop/Left/Width/Height
· clientTop/Left/Width/Height
· getComputedStyle()
· currentStyle(in IE))
這些屬性和方法的訪問和調(diào)用,都會觸發(fā) Reflow 的產(chǎn)生,我們應(yīng)該盡量減少對這些屬性和方法的訪問和調(diào)用,參考如下代碼:
清單 19. 特殊測量屬性
這里我們可以用臨時(shí)變量將"offsetWidth"的值緩存起來,這樣就不用每次訪問"offsetWidth"屬性。這種方式在循環(huán)里面非常適用,可以極大地提高性能。
2.4 樣式相關(guān)
我們肯定經(jīng)常見到如下的代碼:
清單 20. 樣式相關(guān)
但是可以看到,這里的每一個(gè)樣式的改變,都會產(chǎn)生 Reflow。需要減少這種情況的發(fā)生,我們可以這樣做:
解決方案 1:
清單 21. className 解決方案
用 class 替代 style,可以將原有的所有 Reflow 或 Repaint 的次數(shù)都縮減到一個(gè)。
解決方案 2:
清單 22. cssText 解決方案
一次性設(shè)置所有樣式,也是減少 Reflow 提高性能的方法。
2.5 XPath
一個(gè)頁面上往往包含 1000 多頁面元素,在定位具體元素的時(shí)候,往往需要一定的時(shí)間。如果用 id 或 name 定位可能效率不會太慢,如果用元素的一些其他屬性(比如 className 等等)定位,可能效率有不理想了。有的可能只能通過遍歷所有元素(getElementsByTagName)然后過濾才能找到相應(yīng)元素,這就更加低效了,這里我們推薦使用 XPath 查找元素,這是很多瀏覽器本身支持的功能。
清單 23. XPath 解決方案
瀏覽器 XPath 的搜索引擎會優(yōu)化搜索效率,大大縮短結(jié)果返回時(shí)間。
2.6 HTMLCollection 對象
這是一類特殊的對象,它們有點(diǎn)像數(shù)組,但不完全是數(shù)組。下述方法的返回值一般都是 HTMLCollection 對象:
· document.images, document.forms
· getElementsByTagName()
· getElementsByClassName()
這些 HTMLCollection 對象并不是一個(gè)固定的值,而是一個(gè)動態(tài)的結(jié)果。它們是一些比較特殊的查詢的返回值,在如下情況下,它們會重新執(zhí)行之前的查詢而得到新的返回值(查詢結(jié)果),雖然多數(shù)情況下會和前一次或幾次的返回值都一樣:
· Length 屬性
· 具體的某個(gè)成員
所以,HTMLCollection 對象對這些屬性和成員的訪問,比起數(shù)組來要慢很多。當(dāng)然也有例外,Opera 和 Safari 對這種情況就處理的很好,不會有太大性能問題。
參考如下代碼:
清單 24. HTMLConnection 對象
上述兩端代碼,下面的效率比起上面一段要慢很多,因?yàn)槊恳粋€(gè)循環(huán)都會有"items.length"的觸發(fā),也就會導(dǎo)致"document.getElementsByTagName(..)"方法的再次調(diào)用,這便是效率便會大幅度下降的原因。我們可以這樣解決:
清單 25. HTMLConnection 對象解決方案
這樣一來,效率基本與普通數(shù)組一樣。
2.7 動態(tài)創(chuàng)建 script 標(biāo)簽
加載并執(zhí)行一段 JavaScript 腳本是需要一定時(shí)間的,在我們的程序中,有時(shí)候有些 JavaScript 腳本被加載后基本沒有被使用過 (比如:腳本里的函數(shù)從來沒有被調(diào)用等等)。加載這些腳本只會占用 CPU 時(shí)間和增加內(nèi)存消耗,降低 Web 應(yīng)用的性能。所以推薦動態(tài)的加載 JavaScript 腳本文件,尤其是那些內(nèi)容較多,消耗資源較大的腳本文件。
清單 26. 創(chuàng)建 script 標(biāo)簽
寫在最后
這篇文章介紹了 Web 開發(fā)中關(guān)于性能方面需要注意的一些小細(xì)節(jié),從 JavaScript 本身著手,介紹了 JavaScript 中需要避免的一些函數(shù)的使用和編程規(guī)則,比如 eval 的弊端,function scope chain 以及 String 的用法等等,也分享了一些比較推薦的做法,并擴(kuò)展到 JavaScript 對 DOM 操作的性能調(diào)優(yōu),比如利用 Repaint 和 Reflow 的機(jī)制,如何使用特殊測量屬性,樣式相關(guān)的性能調(diào)優(yōu)以及 HTMLCollection 對象的原理和使用小技巧。這些小細(xì)節(jié)我們可以在開發(fā)過程中盡量注意一下,以盡可能多的提高我們 Web 應(yīng)用的性能。