Web性能優(yōu)化系列 10個(gè)提升JavaScript性能的技巧
Nicholas Zakas是一位 JS 大師,Yahoo! 首頁(yè)的前端主程。他是《高性能 Javascript》的作者,這本書(shū)值得每個(gè)程序員去閱讀。
當(dāng)談到 JS 性能的時(shí)候,Zakas差不多就是你要找的,2010年六月他在Google Tech Talk發(fā)表了名為《Speed Up Your Javascript》的演講。
但 Javascript 性能優(yōu)化絕不是一種書(shū)面的技術(shù),Nicholas 的技術(shù)演進(jìn)列出了10條建議,幫助你寫出高效的 JS 代碼。
1. 定義局部變量
當(dāng)一個(gè)變量被引用的時(shí)候,JavaScript將在作用域鏈中的不同成員中查找這個(gè)變量。作用域鏈指的是當(dāng)前作用于下可用變量的集合,它在各種主流瀏覽器中至少包含兩個(gè)部分:局部變量的集合和全局變量的集合。
簡(jiǎn)單地說(shuō),如果JavaScript引擎在作用域鏈中搜索的深度越大,那么操作也就會(huì)消耗更多的時(shí)間。引擎首先從 this 開(kāi)始查找局部變量,然后是函數(shù)參數(shù)、本地定義的變量,最后遍歷所有的全局變量。
因?yàn)榫植孔兞吭谶@條鏈的起端,所以查找局部變量總是比查找全局變量要塊。所以當(dāng)你想要不止一次地使用一個(gè)全局變量的時(shí)候,你應(yīng)該將它定義成局部變量,就像這樣:
var blah = document.getElementById('myID'), blah2 = document.getElementById('myID2');
改寫成
var doc = document, blah = doc.getElementById('myID'), blah2 = doc.getElementById('myID2');
2. 不要使用 with() 語(yǔ)句
這是因?yàn)?with() 語(yǔ)句將會(huì)在作用域鏈的開(kāi)始添加額外的變量。額外的變量意味著,當(dāng)任何變量需要被訪問(wèn)的時(shí)候,JavaScript引擎都需要先掃描with()語(yǔ)句產(chǎn)生的變量,然后才是局部變量,最后是全局變量。
So with() essentially gives local variables all the performance drawbacks of global ones, and in turn derails Javascript optimization. 因此with()語(yǔ)句同時(shí)給局部變量和全局變量的性能帶來(lái)負(fù)面影響,最終使我們優(yōu)化JavaScript性能的計(jì)劃破產(chǎn)。
3. 小心使用閉包
雖然你可能還不知道“閉包”,但你可能在不經(jīng)意間經(jīng)常使用這項(xiàng)技術(shù)。閉包基本上被認(rèn)為是JavaScript中的new,當(dāng)我們定義一個(gè)即時(shí)函數(shù)的時(shí)候,我們就使用了閉包,比如:
document.getElementById('foo').onclick = function(ev) { };
閉包的問(wèn)題在于:根據(jù)定義,在它們的作用域鏈中至少有三個(gè)對(duì)象:閉包變量、局部變量和全局變量。這些額外的對(duì)象將會(huì)導(dǎo)致第1和第2個(gè)建議中提到的性能問(wèn)題。
但是我認(rèn)為Nicholas并不是要我們因噎廢食,閉包對(duì)于提高代碼可讀性等方面還是非常有用的,只是不要濫用它們(尤其在循環(huán)中)。
4. 對(duì)象屬性和數(shù)組元素的速度都比變量慢
談到JavaScript的數(shù)據(jù),一般來(lái)說(shuō)有4種訪問(wèn)方式:數(shù)值、變量、對(duì)象屬性和數(shù)組元素。在考慮優(yōu)化時(shí),數(shù)值和變量的性能差不多,并且速度顯著優(yōu)于對(duì)象屬性和數(shù)組元素。
因此當(dāng)你多次引用一個(gè)對(duì)象屬性或者數(shù)組元素的時(shí)候,你可以通過(guò)定義一個(gè)變量來(lái)獲得性能提升。(這一條在讀、寫數(shù)據(jù)時(shí)都有效)
雖然這條規(guī)則在絕大多數(shù)情況下是正確的,但是Firefox在優(yōu)化數(shù)組索引上做了一些有意思的工作,能夠讓它的實(shí)際性能優(yōu)于變量。但是考慮到數(shù)組元素在其他瀏覽器上的性能弊端,還是應(yīng)該盡量避免數(shù)組查找,除非你真的只針對(duì)于火狐瀏覽器的性能而進(jìn)行開(kāi)發(fā)。
5. 不要在數(shù)組中挖得太深
另外,程序員應(yīng)該避免在數(shù)組中挖得太深,因?yàn)檫M(jìn)入的層數(shù)越多,操作速度就越慢。
簡(jiǎn)單地說(shuō),在嵌套很多層的數(shù)組中操作很慢是因?yàn)閿?shù)組元素的查找速度很慢。試想如果操作嵌套三層的數(shù)組元素,就要執(zhí)行三次數(shù)組元素查找,而不是一次。
因此如果你不斷地引用 foo.bar, 你可以通過(guò)定義 var bar = foo.bar 來(lái)提高性能。
6. 避免 for-in 循環(huán)(和基于函數(shù)的迭代)
這是另一條非常教條的建議:不要使用for-in循環(huán)。
這背后的邏輯非常直接:要遍歷一個(gè)集合內(nèi)的元素,你可以使用諸如for循環(huán)、或者do-while循環(huán)來(lái)替代for-in循環(huán),for-in循環(huán)不僅僅可能需要遍歷額外的數(shù)組項(xiàng),還需要更多的時(shí)間。
為了遍歷這些元素,JavaScript需要為每一個(gè)元素建立一個(gè)函數(shù),這種基于函數(shù)的迭代帶來(lái)了一系列性能問(wèn)題:額外的函數(shù)引入了函數(shù)對(duì)象被創(chuàng)建和銷毀的上下文,將會(huì)在作用域鏈的頂端增加額外的元素。
7. 在循環(huán)時(shí)將控制條件和控制變量合并起來(lái)
提到性能,在循環(huán)中需要避免的工作一直是個(gè)熱門話題,因?yàn)檠h(huán)會(huì)被重復(fù)執(zhí)行很多次。所以如果有性能優(yōu)化的需求,先對(duì)循環(huán)開(kāi)刀有可能會(huì)獲得最明顯的性能提升。
一種優(yōu)化循環(huán)的方法是在定義循環(huán)的時(shí)候,將控制條件和控制變量合并起來(lái),下面是一個(gè)沒(méi)有將他們合并起來(lái)的例子:
for ( var x = 0; x < 10; x++ ) {
};
當(dāng)我們要添加什么東西到這個(gè)循環(huán)之前,我們發(fā)現(xiàn)有幾個(gè)操作在每次迭代都會(huì)出現(xiàn)。JavaScript引擎需要:
#1:檢查 x 是否存在
#2:檢查 x 是否小于 0 (譯者注:我猜這里是作者的筆誤)
#3:使 x 增加 1
然而如果你只是迭代元素中的一些元素,那么你可以使用while循環(huán)進(jìn)行輪轉(zhuǎn)來(lái)替代上面這種操作:
var x = 9; do { } while( x-- );
如果你想更深入地了解循環(huán)的性能,Zakas提供了一種高級(jí)的循環(huán)優(yōu)化技巧,使用異步進(jìn)行循環(huán)(碉堡了?。?/p>
8. 為HTML集合對(duì)象定義數(shù)組
JavaScript使用了大量的HTML集合對(duì)象,比如 document.forms,document.images 等等。通常他們被諸如 getElementsByTagName、getElementByClassName 等方法調(diào)用。
由于大量的DOM selection操作,HTML集合對(duì)象相當(dāng)?shù)穆?,而且還會(huì)帶來(lái)很多額外的問(wèn)題。正如DOM標(biāo)準(zhǔn)中所定義的那樣:“HTML集合是一個(gè)虛擬存在,意味著當(dāng)?shù)讓游臋n被改變時(shí),它們將自動(dòng)更新?!边@太可怕了!
盡管集合對(duì)象看起來(lái)跟數(shù)組很像,他們?cè)谀承┑胤絽s區(qū)別很大,比如對(duì)于特定查詢的結(jié)果。當(dāng)對(duì)象被訪問(wèn)進(jìn)行讀寫時(shí),查詢需要重新執(zhí)行來(lái)更新所有與對(duì)象相關(guān)的組分,比如 length。
HTML集合對(duì)象也非常的慢,Nicholas說(shuō)好像在看球的時(shí)候?qū)σ粋€(gè)小動(dòng)作進(jìn)行60倍速慢放。另外,集合對(duì)象也有可能造成死循環(huán),比如下面的例子:
var divs = document.getElementsByTagName('div'); for (var i=0; i < divs.length; i++ ) { var div = document.createElement("div"); document.appendChild(div); }
這段代碼造成了死循環(huán),因?yàn)?divs 表示一個(gè)實(shí)時(shí)的HTML集合,并不是你所期望的數(shù)組。這種實(shí)時(shí)的集合在添加 <div> 標(biāo)簽時(shí)被更新,所以i < div.length 永遠(yuǎn)都不會(huì)結(jié)束。
解決這個(gè)問(wèn)題的方法是將這些元素定義成數(shù)組,相比只設(shè)置 var divs = document.getElementsByTagName(‘div') 稍微有點(diǎn)麻煩,下面是Zakas提供的強(qiáng)制使用數(shù)組的代碼:
function array(items) { try { return Array.prototype.concat.call(items); } catch (ex) { var i = 0, len = items.length, result = Array(len); while (i < len) { result[i] = items[i]; i++; } return result; } } var divs = array( document.getElementsByTagName('div') ); for (var i=0l i < divs.length; i++ ) { var div = document.createElement("div"); document.appendChild(div); }
9. 不要碰DOM!
不使用DOM是JavaScript優(yōu)化中另一個(gè)很大的話題。經(jīng)典的例子是添加一系列的列表項(xiàng):如果你把每個(gè)列表項(xiàng)分別加到DOM中,肯定會(huì)比一次性加入所有列表項(xiàng)到DOM中要慢。這是因?yàn)镈OM操作開(kāi)銷很大。
Zakas對(duì)這個(gè)進(jìn)行了細(xì)致的講解,解釋了由于回流(reflow)的存在,DOM操作是非常消耗資源的?;亓魍ǔ1焕斫鉃闉g覽器重新選渲染DOM樹(shù)的處理過(guò)程。比如說(shuō),如果你用JavaScript語(yǔ)句改變了一個(gè)div的寬度,瀏覽器需要重繪頁(yè)面來(lái)適應(yīng)變化。
任何時(shí)候只要有元素被添加到DOM樹(shù)或者從DOM樹(shù)移除,都會(huì)引發(fā)回流。使用一個(gè)非常方便的JavaScript對(duì)象可以解決這個(gè)問(wèn)題——documentFragment,我并沒(méi)有使用過(guò),但是在Steve Souders也表示同意這種做法之后我感覺(jué)更加肯定了。
DocumentFragment 基本上是一種瀏覽器以非可視方式實(shí)現(xiàn)的類似文檔的片段,非可視化的表現(xiàn)形式帶來(lái)了很多優(yōu)點(diǎn),最主要的是你可以在 documentFragment 中添加任何結(jié)點(diǎn)而不會(huì)引起瀏覽器回流。
10. 修改CSS類,而不是樣式
你也許聽(tīng)說(shuō)過(guò):修改CSS類必直接修改樣式會(huì)更高效。這歸結(jié)于回流帶來(lái)的另一個(gè)問(wèn)題:當(dāng)布局樣式發(fā)生改變時(shí),會(huì)引發(fā)回流。
布局樣式意味著任何影響改變布局的變化都會(huì)強(qiáng)制引起瀏覽器回流。比如寬度、高度、字號(hào)、浮動(dòng)等。
但是別誤會(huì)我的意思,CSS類并不會(huì)避免回流,但是可以將它的影響最小化。相比每次修改樣式都會(huì)引起回流,使用CSS類一次修改多個(gè)樣式,只需要承擔(dān)一次回流帶來(lái)的消耗。
因此在修改多個(gè)布局樣式的時(shí)候,使用CSS類來(lái)優(yōu)化性能是明智的選擇。另外如果你需要在運(yùn)行時(shí)定義很多歌CSS類,在DOM上添加樣式結(jié)點(diǎn)也是不錯(cuò)的選擇。
總結(jié)
Nicholas C. Zakas 是JavaScript界的權(quán)威。在寫這篇文章的時(shí)候,我發(fā)現(xiàn)我引用的很多文章也是他寫的——因?yàn)樘y找到其他更好的文章。
Zakas的技術(shù)演進(jìn)非常棒,他解釋了很多JavaScript優(yōu)化規(guī)則的原因,我已奉為圣經(jīng)。
- Vue.js 無(wú)限滾動(dòng)列表性能優(yōu)化方案
- 基于Nuxt.js項(xiàng)目的服務(wù)端性能優(yōu)化與錯(cuò)誤檢測(cè)(容錯(cuò)處理)
- 原生JS實(shí)現(xiàn)圖片懶加載之頁(yè)面性能優(yōu)化
- react性能優(yōu)化達(dá)到最大化的方法 immutable.js使用的必要性
- JavaScript性能優(yōu)化之函數(shù)節(jié)流(throttle)與函數(shù)去抖(debounce)
- 整理AngularJS框架使用過(guò)程當(dāng)中的一些性能優(yōu)化要點(diǎn)
- javascript性能優(yōu)化之事件委托實(shí)例詳解
- JS性能優(yōu)化實(shí)現(xiàn)方法及優(yōu)點(diǎn)進(jìn)行
相關(guān)文章
查找頁(yè)面中所有類為test的結(jié)點(diǎn)的方法
這篇文章主要介紹了查找頁(yè)面中所有類為test結(jié)點(diǎn)的方法,需要的朋友可以參考下2014-03-03VSCode 添加自定義注釋的方法(附帶紅色警戒經(jīng)典注釋風(fēng)格)
這篇文章主要介紹了VSCode 添加自定義注釋的方法(附帶紅色警戒經(jīng)典注釋風(fēng)格),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08JS實(shí)現(xiàn)獲取自定義屬性data值的方法示例
這篇文章主要介紹了JS實(shí)現(xiàn)獲取自定義屬性data值的方法,結(jié)合實(shí)例形式較為詳細(xì)的分析了javascript針對(duì)自定義屬性data的相關(guān)操作技巧,需要的朋友可以參考下2018-12-12使用OPENLAYERS3實(shí)現(xiàn)點(diǎn)選的方法
這篇文章主要為大家詳細(xì)介紹了使用OPENLAYERS3實(shí)現(xiàn)點(diǎn)選的幾種方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07