加載和執(zhí)行
腳本位置
放在<head>中的javascript文件會(huì)阻塞頁面渲染:一般來說瀏覽器中有多種線程:UI渲染線程、javascript引擎線程、瀏覽器事件觸發(fā)線程、HTTP請(qǐng)求線程等。多線程之間會(huì)共享運(yùn)行資源,瀏覽器的js會(huì)操作dom,影響渲染,所以js引擎線程和UI渲染線程是互斥的,導(dǎo)致執(zhí)行js時(shí)會(huì)阻塞頁面的渲染。
最佳實(shí)踐:所有的Script標(biāo)簽盡可能放在body標(biāo)簽底部,以盡量減少對(duì)整個(gè)頁面下載的影響
組織腳本
每個(gè)<script>標(biāo)簽初始下載時(shí)都會(huì)阻塞頁面渲染,所以應(yīng)減少頁面包含的<script>標(biāo)簽數(shù)量。內(nèi)嵌腳本放在引用外鏈樣式表的<link>標(biāo)簽之后會(huì)導(dǎo)致頁面阻塞去等待樣式表的下載,建議不要把內(nèi)嵌腳本緊跟在<link>標(biāo)簽之后。外鏈javascript的HTTP請(qǐng)求還會(huì)帶來額外的性能開銷,減少腳本文件的數(shù)量將會(huì)改善性能。
無阻塞的腳本
無阻塞腳本的意義在于在頁面加載完成后才加載JavaScript代碼。(windows對(duì)象的load事件觸發(fā)后)
延遲的腳本
帶有defer屬性的<script>標(biāo)簽可以放置在文檔的任何位置。對(duì)應(yīng)的Javascript文件將在頁面解析到<script>標(biāo)簽時(shí)開始下載,但是并不會(huì)執(zhí)行,知道DOM加載完成(onload事件被觸發(fā)前)。當(dāng)一個(gè)帶有defer屬性的JavaScript文件下載時(shí),它將不會(huì)阻塞瀏覽器的其他進(jìn)程,可以與其他資源并行下載。執(zhí)行的順序是script、defer、load。
動(dòng)態(tài)腳本元素
使用JavaScript動(dòng)態(tài)創(chuàng)建HTML中script元素,例如一些懶加載庫。
優(yōu)點(diǎn):動(dòng)態(tài)腳本加載憑借著它在跨瀏覽器兼容性和易用的優(yōu)勢,成為最通用的無阻塞加載解決方式。
XHR腳本注入
創(chuàng)建XHR對(duì)象,用它下載JavaScript文件,通過動(dòng)態(tài)創(chuàng)建script元素,將代買注入頁面中
var xhr = new XMLHttpRequest();
xhr.open("get","file.js",true);
xhr.onreadystatechange = function() {
if(xht.readyState === 4) {
if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
var script = document.createElement("script");
script.type = "text/javascript";
script.text = xhr.responseText;
document.body.appendChild(script);
}
}
};
xhr.send(null);
優(yōu)點(diǎn):可以下載JavaScript但不立即執(zhí)行,在所有主流瀏覽器中都可以正常工作。
缺點(diǎn):JavaScript文件必須與所請(qǐng)求的頁面處于相同的域,意味著文件從CDN下載。
數(shù)據(jù)存取
存儲(chǔ)的位置
數(shù)據(jù)存儲(chǔ)的位置會(huì)很大程度上影響讀取速度。
- 字面量:字面量只代表自身,不存儲(chǔ)在特定的位置。包括:字符串、數(shù)字、布爾值、對(duì)象、數(shù)組、函數(shù)、正則表達(dá)式、null、undefined。(個(gè)人理解:對(duì)象的指針本身是字面量)。
- 本地變量:var定義的數(shù)據(jù)存儲(chǔ)單元。
- 數(shù)組元素:存儲(chǔ)在JavaScript數(shù)組內(nèi)部,以數(shù)字為引。
- 對(duì)象成員:存儲(chǔ)在JavaScript對(duì)象內(nèi)部,以字符串作為索引。
大多數(shù)情況下從一個(gè)字面量和一個(gè)局部變量中存取數(shù)據(jù)的差異是微不足道的。訪問數(shù)據(jù)元素和對(duì)象成員的代價(jià)則搞一點(diǎn)。如果在乎運(yùn)行速度,盡量使用字面量和局部變量,減少數(shù)組和對(duì)象成員的使用。
管理作用域
作用域鏈
每個(gè)JavaScript函數(shù)都表示為一個(gè)對(duì)象,更確切得說是Function對(duì)象的一個(gè)實(shí)例。它也有僅供JavaScript引擎存儲(chǔ)的內(nèi)部屬性,其中一個(gè)內(nèi)部屬性是[[Scope]],包涵了一個(gè)被創(chuàng)建的作用域中對(duì)象的集合,即作用域鏈。作用雨量決定哪些數(shù)據(jù)能被函數(shù)訪問。作用域中的每個(gè)對(duì)象被稱為一個(gè)可變對(duì)象。
當(dāng)一個(gè)函數(shù)被創(chuàng)建后,作用域鏈會(huì)被創(chuàng)建函數(shù)的作用域中可訪問的數(shù)據(jù)對(duì)象所填充。執(zhí)行函數(shù)時(shí)會(huì)創(chuàng)建一個(gè)稱為執(zhí)行上下文的內(nèi)部對(duì)象。執(zhí)行上下文定義了函數(shù)執(zhí)行時(shí)的環(huán)境。每次函數(shù)執(zhí)行時(shí)對(duì)應(yīng)的執(zhí)行環(huán)境都是獨(dú)一無二的,多次調(diào)用同一個(gè)函數(shù)也會(huì)創(chuàng)建多個(gè)執(zhí)行上下文,當(dāng)函數(shù)執(zhí)行完畢,執(zhí)行上下文會(huì)被銷毀。每個(gè)執(zhí)行上下文都有自己的作用域鏈,用于解析標(biāo)識(shí)符。當(dāng)執(zhí)行上下文被創(chuàng)建時(shí),它的作用域鏈初始化成當(dāng)前運(yùn)行函數(shù)的[[scope]]屬性中的對(duì)象。這些值哪找它們出現(xiàn)在函數(shù)中的順序,被復(fù)制到執(zhí)行環(huán)境的作用域鏈中。這個(gè)過程一旦完成,一個(gè)被稱為活動(dòng)對(duì)象的新對(duì)象就為執(zhí)行上下文創(chuàng)建好了。
活動(dòng)對(duì)象作為函數(shù)運(yùn)行時(shí)的變量對(duì)象,包含了所有局部對(duì)象,命名函數(shù),參數(shù)集合以及this。然后次對(duì)象被推入作用域鏈的最前端。當(dāng)執(zhí)行環(huán)境被銷毀時(shí),活動(dòng)對(duì)象也隨之銷毀。執(zhí)行過程中每遇到一個(gè)變量,都會(huì)經(jīng)歷一次標(biāo)識(shí)符解析過程以決定從哪里獲取或存儲(chǔ)數(shù)據(jù)。該過程搜索執(zhí)行環(huán)境的作用域鏈,查找同名的標(biāo)識(shí)符。搜索過程中從作用域鏈頭部開始,也就是當(dāng)前運(yùn)行函數(shù)的活動(dòng)對(duì)象。如果找到,就是用這個(gè)標(biāo)識(shí)符對(duì)應(yīng)的變量,如果沒找到,繼續(xù)搜索作用域鏈的下一個(gè)對(duì)象直到找到,若無法搜索到匹配的對(duì)象,則標(biāo)識(shí)符被當(dāng)做未定義的。這個(gè)搜索過程影響了性能。
標(biāo)識(shí)符解析的性能
一個(gè)標(biāo)識(shí)符所在的位置越深,讀寫速度就越慢,全局變量總是存在于執(zhí)行環(huán)境作用域的最末端,因此它是最深的。
最佳實(shí)踐:如果某個(gè)跨作用域的值在函數(shù)中被引用一次以上,那么就把它存儲(chǔ)到局部變量中。
改變作用域鏈
一般來說一個(gè)執(zhí)行上下文的作用域鏈?zhǔn)遣粫?huì)改變的。但是,with語句和try-catch語句的catch子語句可以改變作用域鏈。
with語句用來給對(duì)象的所有屬性創(chuàng)建一個(gè)變量,可以避免多次書寫。但是存在性能問題:代碼執(zhí)行到with語句時(shí),執(zhí)行環(huán)境的作用域鏈臨時(shí)被改變了,創(chuàng)建了一個(gè)新的(包含了with對(duì)象所有屬性)對(duì)象被創(chuàng)建了,之前所有的局部變量現(xiàn)在處于第二個(gè)作用域鏈對(duì)象中,提高了訪問的代價(jià)。建議放棄使用with語句。
try-catch語句中的catch子句也可以改變作用域鏈,當(dāng)try代碼塊中發(fā)生錯(cuò)誤,執(zhí)行過程會(huì)自動(dòng)跳轉(zhuǎn)到catch子句,把異常對(duì)象推入一個(gè)變量對(duì)象并置于作用域的首位,局部變量處于第二個(gè)作用域鏈對(duì)象中。簡化代碼可以使catch子句對(duì)性能的影響降低。
最佳實(shí)踐:將錯(cuò)誤委托給一個(gè)函數(shù)來處理。
動(dòng)態(tài)作用域
無論with語句還是try-catch語句的子句catch子句、eval()語句,都被認(rèn)為是動(dòng)態(tài)作用域。經(jīng)過優(yōu)化的JavaScript引擎,嘗試通過分析代碼來確定哪些變量是可以在特定的時(shí)候被訪問,避開了傳統(tǒng)的作用域鏈,取代以標(biāo)識(shí)符索引的方式快速查找。當(dāng)涉及動(dòng)態(tài)作用域時(shí),這種優(yōu)化方式就失效了。
最佳實(shí)踐:只在確實(shí)有必要時(shí)使用動(dòng)態(tài)作用域。
閉包、作用域和內(nèi)存
由于閉包的[[Scope]]屬性包含了與執(zhí)行上下文作用域鏈相同的對(duì)象的引用,因此會(huì)產(chǎn)生副作用。通常來說,函數(shù)的活動(dòng)對(duì)象會(huì)隨著執(zhí)行環(huán)境一同銷毀。但引入閉包時(shí),由于引用仍然存在閉包的[[Scope]]屬性中,因此激活對(duì)象無法被銷毀,導(dǎo)致更多的內(nèi)存開銷。
最需要關(guān)注的性能點(diǎn):閉包頻繁訪問跨作用域的標(biāo)識(shí)符,每次訪問都會(huì)帶來性能損失。
最佳實(shí)踐:將常用的跨作用域變量存儲(chǔ)在局部變量中,然后直接訪問局部變量。
對(duì)象成員
無論是通過創(chuàng)建自定義對(duì)象還是使用內(nèi)置對(duì)象都會(huì)導(dǎo)致頻繁的訪問對(duì)象成員。
原型
JavaScript中的對(duì)象是基于原型的。解析對(duì)象成員的過程與解析變量十分相似,會(huì)從對(duì)象的實(shí)例開始,如果實(shí)例中沒有,會(huì)一直沿著原型鏈向上搜索,直到找到或者到原型鏈的盡頭。對(duì)象在原型鏈中位置越深,找到它也就越慢。搜索實(shí)例成員比從字面量或局部變量中讀取數(shù)據(jù)代價(jià)更高,再加上遍歷原型鏈帶來的開銷,這讓性能問題更為嚴(yán)重。
嵌套成員
對(duì)象成員可能包含其他成員,每次遇到點(diǎn)操作符"."會(huì)導(dǎo)致JavaScript引擎搜索所有對(duì)象成員。
緩存對(duì)象成員值
由于所有類似的性能問題都與對(duì)象成員有關(guān),因此應(yīng)該盡可能避免使用他們,只在必要時(shí)使用對(duì)象成員,例如,在同一個(gè)函數(shù)中沒有必要多次讀取同一個(gè)對(duì)象屬性(保存到局部變量中),除非它的值變了。這種方法不推薦用于對(duì)象的方法,因?yàn)閷?duì)象方法保存在局部變量中會(huì)導(dǎo)致this綁定到window,導(dǎo)致JavaScript引擎無法正確的解析它的對(duì)象成員,進(jìn)而導(dǎo)致程序出錯(cuò)。
DOM編程
瀏覽器中的DOM
文檔對(duì)象模型(DOM)是一個(gè)獨(dú)立于語言的,用于操作XML和HTML文檔的程序接口API。DOM是個(gè)與語言無關(guān)的API,在瀏覽器中的接口是用JavaScript實(shí)現(xiàn)的。客戶端腳本編程大多數(shù)時(shí)候是在和底層文檔打交道,DOM就成為現(xiàn)在JavaScript編碼中的重要組成部分。瀏覽器把DOM和JavaScript單獨(dú)實(shí)現(xiàn),使用不同的引擎。
天生就慢
DOM和javascript就像兩個(gè)島嶼通過收費(fèi)橋梁連接,每次通過都要繳納“過橋費(fèi)”。
推薦的做法是盡可能減少過橋的次數(shù),努力待在ECMAScript島上。
DOM訪問與修改
訪問DOM元素是有代價(jià)的——前面的提到的“過橋費(fèi)”。修改元素則更為昂貴,因?yàn)樗鼤?huì)導(dǎo)致瀏覽器重新計(jì)算頁面的幾何變化(重排)。最壞的情況是在循環(huán)中訪問或修改元素,尤其是對(duì)HTML元素集合循環(huán)操作。
在循環(huán)訪問頁面元素的內(nèi)容時(shí),最佳實(shí)踐是用局部變量存儲(chǔ)修改中的內(nèi)容,在循環(huán)結(jié)束后一次性寫入。
通用的經(jīng)驗(yàn)法則是:減少訪問DOM的次數(shù),把運(yùn)算盡量留在ECMAScript中處理。
節(jié)點(diǎn)克隆
大多數(shù)瀏覽器中使用節(jié)點(diǎn)克隆都比創(chuàng)建新元素要更有效率。
選擇API
使用css選擇器也是一種定位節(jié)點(diǎn)的便利途徑,瀏覽器提供了一個(gè)名為querySelectorAll()的原生DOM方法。這種方法比使用JavaScript和DOM來遍歷查找元素快很多。使用另一個(gè)便利方法——querySelector()來獲取第一個(gè)匹配的節(jié)點(diǎn)。
重繪與重排
瀏覽器下載完頁面中的所有組件——HTML標(biāo)記、JavaScript、CSS、圖片——之后會(huì)解析并生成兩個(gè)內(nèi)部的數(shù)據(jù)結(jié)構(gòu):DOM樹(表示頁面結(jié)構(gòu))、渲染樹(表示DOM節(jié)點(diǎn)如何顯示)。當(dāng)DOM的變化影響了元素的幾何屬性,瀏覽器會(huì)使渲染樹中受到影響的部分失效,并重構(gòu),這個(gè)過程成為重排,完成后,會(huì)重新繪制受影響的部分到屏幕,該過程叫重繪。并不是所有的DOM變化都會(huì)影響幾何屬性,這時(shí)只發(fā)生重繪。重繪和重排會(huì)導(dǎo)致web應(yīng)用程序的UI反應(yīng)遲鈍,應(yīng)該盡量避免。
重排何時(shí)發(fā)生
當(dāng)頁面布局的幾何屬性改變時(shí)就需要重排:
- 添加或刪除可見的DOM元素
- 元素位置改變
- 元素尺寸改變(包括:外邊據(jù)、內(nèi)邊距、邊框厚度、寬度、高度等屬性改變)
- 內(nèi)容改變,例如:文本改變或圖片被另一個(gè)不同尺寸的圖片代替
- 頁面渲染器初始化
- 瀏覽器窗口尺寸改變
渲染樹變化的排隊(duì)與刷新
由于每次重排都會(huì)產(chǎn)生計(jì)算消耗,大多數(shù)瀏覽器通過隊(duì)列化修改并批量執(zhí)行來優(yōu)化重排過程。但是有些操作會(huì)導(dǎo)致強(qiáng)制刷新隊(duì)列并要求任務(wù)立刻執(zhí)行:
1. offsetTop,offsetLeft,offsetWidth,offsetHeight
2. scrollTop,scrollLeft,scrollWidth,scrollHeight
3. clientTop,clientLeft,clientWidth,clientHeight
4. getComputedStyle()
以上屬性和方法需要返回最新的布局信息,因此瀏覽器不得不執(zhí)行渲染隊(duì)列中的修改變化并觸發(fā)重排以返回正確的值。
最佳實(shí)踐:盡量將修改語句放在一起,查詢語句放在一起。
最小化重繪和重排
為了減少發(fā)生次數(shù),應(yīng)該合并多次DOM的樣式的修改,然后一次處理掉。
批量修改DOM
當(dāng)你需要對(duì)DOM元素進(jìn)行一系列操作時(shí),可以通過以下步驟來減少重繪和重排的次數(shù):
- 使元素脫離文檔
- 對(duì)其應(yīng)用多重改變
- 把元素帶回文檔流
該過程會(huì)觸發(fā)兩次重排——第一步和第三步,如果忽略這兩步,在第二步所產(chǎn)生的任何修改都會(huì)觸發(fā)一次重排。
有三種基本的方法可以使DOM脫離文檔:
- 隱藏元素,應(yīng)用修改,重新顯示
- 使用文檔片段,在當(dāng)前DOM之外構(gòu)建一個(gè)子樹,再把它拷貝回文檔
- 將原始元素拷貝到一個(gè)脫離文檔的節(jié)點(diǎn)中,修改副本,完成后再替換原始元素
推薦使用文檔片段,因?yàn)樗鼈兯a(chǎn)生的DOM遍歷和重排次數(shù)最少。
緩存緩存布局信息
當(dāng)你查詢布局信息時(shí),瀏覽器為了返回最新值,會(huì)刷新隊(duì)列并應(yīng)用所有變更。
最佳實(shí)踐:盡量減少布局信息的獲取次數(shù),獲取后把它賦值給局部變量,然后操作局部變量。
讓元素脫離動(dòng)畫流
用展開、折疊的方式來顯示和隱藏部分頁面是一種常見的交互模式。通常包括展開區(qū)域的幾何動(dòng)畫,并將頁面其他部分推向下方。一般來說,重排只影響渲染樹中的一小部分,但也可能影響很大的部分,甚至整個(gè)渲染樹。瀏覽器所需要重排的次數(shù)越少,應(yīng)用程序的響應(yīng)速度就越快。當(dāng)一個(gè)動(dòng)畫改變整個(gè)頁面的余下部分時(shí),會(huì)導(dǎo)致大規(guī)模重排。節(jié)點(diǎn)越多情況越差。避免大規(guī)模的重排:
1. 使用絕對(duì)定位頁面上的動(dòng)畫元素,將其脫離文檔流。
2. 應(yīng)用動(dòng)畫
3. 當(dāng)動(dòng)畫結(jié)束時(shí)回恢復(fù)定位,從而只會(huì)下移一次文檔的其他元素。
這樣只造成了頁面的一個(gè)小區(qū)域的重繪,不會(huì)產(chǎn)生重排并重繪頁面的大部分內(nèi)容。
:hover
如果有大量元素使用了:hover,那么會(huì)降低響應(yīng)速度。此問題在IE8中更為明顯。
事件委托
當(dāng)頁面中存在大量元素,并且每一個(gè)都要一次或多次綁定事件處理器時(shí),這種情況可能會(huì)影響性能,每綁定一個(gè)事件處理器都是有代價(jià)的,它要么加重了頁面負(fù)擔(dān)(更多的代碼、標(biāo)簽),要么增加了運(yùn)行期的執(zhí)行時(shí)間。需要訪問和修改的DOM元素越多,應(yīng)用程序就越慢,特別是事件綁定通常發(fā)生在onload時(shí),此時(shí)對(duì)每一個(gè)富交互應(yīng)用的網(wǎng)頁來說都是一個(gè)擁堵的時(shí)刻。事件綁定占用了處理事件,而且瀏覽器要跟蹤每個(gè)事件處理器,這也會(huì)占用更多的內(nèi)存。這些事件處理器中的絕大部分都可能不會(huì)被觸發(fā)。
事件委托原理:事件逐層冒泡并能被父級(jí)元素捕獲。使用事件代理,只需要給外層元素綁定一個(gè)處理器,就可以處理在其子元素上觸發(fā)的所有事件。
根據(jù)DOM標(biāo)準(zhǔn),每個(gè)事件都要經(jīng)歷三個(gè)階段:
1. 捕獲
2. 到達(dá)目標(biāo)
3. 冒泡
IE不支持捕獲,但是對(duì)于委托而言,冒泡已經(jīng)足夠。
<body>
<div>
<ul id="menu">
<li>
<a href="menu1.html">menu #1</a>
</li>
<li>
<a href="menu1.html">menu #2</a>
</li>
</ul>
</div>
</body>
在以上的代碼中,當(dāng)用戶點(diǎn)擊鏈接“menu #1”,點(diǎn)擊事件首先從a標(biāo)簽元素收到,然后向DOM樹上層冒泡,被li標(biāo)簽接收然后是ul標(biāo)簽然后是div標(biāo)簽,一直到達(dá)document的頂層甚至window。
委托實(shí)例:阻止默認(rèn)行為(打開鏈接),只需要給所有鏈接的外層UL"menu"元素添加一個(gè)點(diǎn)擊監(jiān)聽器,它會(huì)捕獲并分析點(diǎn)擊是否來自鏈接。
document.getElementById('menu').onclick = function(e) {
//瀏覽器target
e=e||window.event;
var target = e.target||e.srcElement;
var pageid,hrefparts;
//只關(guān)心hrefs,非鏈接點(diǎn)擊則退出,注意此處是大寫
if (target.nodeName !== 'A') {
return;
}
//從鏈接中找出頁面ID
hrefparts = target.href.split('/');
pageid = hrefparts[hrefparts.length-1];
pageid = pageid.replace('.html','');
//更新頁面
ajaxRequest('xhr.php?page='+id,updatePageContents);
//瀏覽器阻止默認(rèn)行為并取消冒泡
if (type of e.preventDefault === 'function') {
e.preventDefault();
e.stopPropagation();
} else {
e.returnValue=false;
e.cancelBubble=true;
}
};
跨瀏覽器兼容部分:
1. 訪問事件對(duì)象,并判斷事件源
2. 取消文檔樹中的冒泡(可選)
3. 阻止默認(rèn)動(dòng)作(可選)
算法和流程控制
循環(huán)
循環(huán)的類型
ECMA-262標(biāo)準(zhǔn)第三版定義了javascript的基本語法和行為,其中共有四種循環(huán)。
-
第一種是標(biāo)準(zhǔn)的for循環(huán)。for循環(huán)是javascript最常用的循環(huán)結(jié)構(gòu),直觀的代碼封裝風(fēng)格被開發(fā)者喜愛。它由四部分組成:初始化、前測條件、后執(zhí)行體、循環(huán)體。
for (var i=0;i<10;i++){ //do something } while循環(huán)。while循環(huán)是最簡單的前測循環(huán),由一個(gè)前測條件和一個(gè)循環(huán)體構(gòu)成。
do-while循環(huán)是javascript唯一一種后測循環(huán),由一個(gè)循環(huán)體和一個(gè)后測條件組成,至少會(huì)執(zhí)行一次。
for-in循環(huán)??梢悦杜e任何對(duì)象的屬性名。
循環(huán)的性能
JavaScript提供的四種循環(huán)類型中,只有for-in循環(huán)比其他幾種明顯要慢。因?yàn)槊看蔚僮鲿?huì)同時(shí)搜索實(shí)例或原型屬性,for-in循環(huán)的每次迭代都會(huì)產(chǎn)生更多開銷。速度只有其他類型循環(huán)的七分之一。除非你明確需要迭代一個(gè)屬性數(shù)量未知的對(duì)象,否則應(yīng)該避免使用for-in循環(huán)。如果你需要遍歷一個(gè)數(shù)量有限的已知屬性列表,使用其他循環(huán)類型會(huì)更快,比如數(shù)組。
除for-in外,其他循環(huán)類型的性能都差不多,類型的選擇應(yīng)該基于需求而不是性能。
提高循環(huán)的性能
- 減少每次迭代處理的事務(wù)
- 減少迭代的次數(shù)