用JS進(jìn)行DOM操作的代價(jià)是昂貴的峰鄙,它是富web應(yīng)用中最常見(jiàn)的性能瓶頸。
DOM
文檔對(duì)象模型(DOM)是一個(gè)獨(dú)立于語(yǔ)言的,用于操作XML和HTML文檔的程序接口(API)谍失。通常在瀏覽器中DOM和JS都是獨(dú)立的晰筛,因?yàn)楸舜霜?dú)立嫡丙,所以JS操作DOM拴袭,性能開(kāi)銷就很大。
提升性能最佳實(shí)踐
- 減少操作
典型場(chǎng)景曙博,循環(huán)操作DOM拥刻,改成循環(huán)拼接,最后操作DOM父泳。
//循環(huán)操作DOM15000次
function innerHTMLLoop(){
for(var count = 0; count < 15000; count++){
document.getElementById('here').innerHTML += 'a';
}
}
//只操作1次DOM般哼,在IE8中性能提升273倍
function innerHTMLLoop(){
var content = '';
for(var count = 0; count < 15000; count++){
content += 'a';
}
document.getElementById('here').innerHTML = content;
}
- 增加新元素時(shí)innerHTML性能高于DOM方法
var newDiv = "<div></div>";
document.getElementById('here').innerHTML = newDiv;
//
var newElement = document.createElement('div');
document.getElementById('here').appendChild(newElement);
- 使用節(jié)點(diǎn)clone
var newElement1 = document.createElement('div');
var newElementN = newElement1.cloneNode(true);
- HTML集合使用array代替
DOM查詢方法(getElementByName,getElementByClassName惠窄,getElementByTagName)蒸眠,以及部分屬性(images,links,forms,elements)返回值是HTML集合(類似數(shù)組,有l(wèi)ength和下標(biāo)訪問(wèn)杆融,無(wú)push楞卡,slice)。這些集合要避免重復(fù)訪問(wèn)脾歇,因?yàn)槊看卧L問(wèn)都會(huì)重新執(zhí)行查詢蒋腮。
//由于每次添加div后,長(zhǎng)度增加介劫,此循環(huán)是死循環(huán)
var divList= document.getElementByName('div')徽惋;
for(var count = 0; count < divList.length; count++){
document.body.appendChild(document.createElement('div'));
}
//集合保存到變量,避免重復(fù)查詢
for(var count = 0; count < document.getElementByName('div').length; count++){
//do something
}
var divList= document.getElementByName('div')座韵;
var len = divList.length
var arr = toArray(divList); //toArray是自定義的函數(shù)险绘,將集合轉(zhuǎn)成array
for(var count = 0; count < len; count++){
//do something
}
注意:此方法會(huì)額外增加一次遍歷操作,長(zhǎng)度小的集合可能不會(huì)提升反而下降
- 局部變量替代
for(var i = 0; i < document.getElementsByTagName("a").length; i++){
document.getElementsByTagName("a")[i].class = 'active'
}
//改進(jìn)后
var list = document.getElementsByTagName("a");
var len = list.length;
for(var i = 0; i < len; i++){
list[i].class = 'active'
}
- IE6誉碴、7中遍歷DOM宦棺,nextSibling性能高于childNode
- 遍歷元素節(jié)點(diǎn)優(yōu)選元素節(jié)點(diǎn)屬性
childNodes,firstChild和nextSibling這些屬性并不區(qū)分元素節(jié)點(diǎn)和其他類型節(jié)點(diǎn)(比如注釋和文本節(jié)點(diǎn))。如果只需要查詢?cè)毓?jié)點(diǎn)黔帕,優(yōu)先使用如下方法代替代咸。(注意:IE6-8只支持children屬性)
元素節(jié)點(diǎn)屬性名 | 被替代的屬性名 |
---|---|
children | childNodes |
childElementCount | childNodes.length |
firstElementChild | firstChild |
lastElementChild | lastChild |
nextElementSibling | nextSibling |
previousElementSibling | previousSibling |
- 利用CSS選擇器提高查找效率
querySelectorAll()方法使用CSS選擇器作為參數(shù),并且返回匹配節(jié)點(diǎn)的類數(shù)組對(duì)象成黄。不會(huì)返回HTML集合呐芥,不會(huì)對(duì)應(yīng)實(shí)時(shí)的文檔結(jié)構(gòu),避免了之前討論的HTML集合引起的性能和邏輯問(wèn)題奋岁。代碼示例如下:
//改進(jìn)前
var els = document.getElementsById("menu").getElementsByTagName("a");
//改進(jìn)后
var els = document.querySelectorAll("#menu a");
如果是組合查詢思瘟,querySelectorAll()方法更具優(yōu)勢(shì)。對(duì)比一下:
//改進(jìn)前
var els = [];
var divs = document.getElementsByTagName("div");
var className = "";
for(var i = 0,len = divs.length; i<len;i++){
className = divs[i].className;
if(className === 'warning' || className === 'notice'){
els.push(divs[i]);
}
}
//改進(jìn)后
var els = document.querySelectorAll("div.warning, div.notice");
推薦使用querySelector()方法闻伶,查詢第一個(gè)匹配的節(jié)點(diǎn)滨攻。
重繪與重排
瀏覽器下載完所有的組件文件(html、js、css光绕、圖片)之后女嘲,會(huì)解析這些組件,并生成兩個(gè)數(shù)據(jù)結(jié)構(gòu):
- DOM樹(shù):表示頁(yè)面結(jié)構(gòu)
- 渲染樹(shù):表示DOM節(jié)點(diǎn)如何顯示
DOM樹(shù)中的每一個(gè)需要顯示的節(jié)點(diǎn)在渲染樹(shù)中至少存在一個(gè)對(duì)應(yīng)的節(jié)點(diǎn)(隱藏的DOM元素在渲染樹(shù)中沒(méi)有對(duì)應(yīng)的節(jié)點(diǎn))诞帐。渲染樹(shù)中的節(jié)點(diǎn)被稱為“幀”或者“盒”欣尼,具有內(nèi)邊距padding,外邊距margin景埃,邊框border和位置position(IE盒模型的高度和寬度包括邊框和內(nèi)邊距媒至,W3C只是內(nèi)容部分。W3C盒模型可使用box-sizing:border-box改成IE盒模型)谷徙。一旦DOM樹(shù)和渲染樹(shù)構(gòu)建完成拒啰,瀏覽器就開(kāi)始顯示(繪制)頁(yè)面元素。
當(dāng)DOM的變化影響了元素的幾何屬性(寬和高)完慧,瀏覽器需要重新計(jì)算元素的集合屬性谋旦,同事其他元素的集合屬性和位置也會(huì)受到影響。瀏覽器會(huì)使渲染樹(shù)中受到影響的部分失效屈尼,并重新構(gòu)造渲染樹(shù)册着。這個(gè)過(guò)程被稱為“重排reflow”。完成重排后脾歧,瀏覽器會(huì)重新繪制受影響的部分到平路中甲捏,該過(guò)程被稱為“重繪repaint”。
并不是所有的DOM變化都會(huì)影響幾何屬性鞭执,比如改變背景色司顿,此時(shí)只會(huì)執(zhí)行重繪而不會(huì)觸發(fā)重排。重繪和重排都是代價(jià)昂貴的操作兄纺,需要盡量避免大溜。
觸發(fā)重排的操作
- 添加和刪除可見(jiàn)的DOM元素
- 元素位置改變
- 元素尺寸變化
- 內(nèi)容改變
- 頁(yè)面渲染器初始化
- 瀏覽器窗口尺寸改變
- 滾動(dòng)條的出現(xiàn)和消失會(huì)觸發(fā)整個(gè)頁(yè)面的重排
渲染樹(shù)變化的排隊(duì)與刷新
由于重排消耗大,大多數(shù)瀏覽器都會(huì)通過(guò)隊(duì)列化修改并批量執(zhí)行來(lái)優(yōu)化重排過(guò)程估脆。獲取布局的如下操作會(huì)導(dǎo)致隊(duì)列刷新:
- offsetTop钦奋,offsetLeft,offsetWidth疙赠,offsetHeight
- scrollTop付材,scrollLeft,scrollWidth圃阳,scrollHeight
- clientTop伞租,clientLeft,clientWidth限佩,clientHeight
- getCumputedStyle()(currentStyle in IE)
執(zhí)行這些屬性和方法需要返回最新的布局信息,因此瀏覽器會(huì)執(zhí)行渲染隊(duì)列中的操作,已獲得最新的布局信息祟同。因此不需要避免頻繁執(zhí)行這些屬性和方法作喘。
最小化重排和重繪
為減少重排或者重繪,應(yīng)該合并多次對(duì)DOM和樣式的修改晕城,然后一次性處理泞坦。
修改樣式
//優(yōu)化前,執(zhí)行了三次重排。大部分現(xiàn)代瀏覽器進(jìn)行了優(yōu)化砖顷,可能只執(zhí)行一次
var el = document.getElementById('mydiv');
el.style.borderLeft='1px';
el.style.borderRight='2px';
el.style.padding='5px';
//優(yōu)化后贰锁,只執(zhí)行一次
var el = document.getElementById('mydiv');
el.style.ccsText='border-left:1px;border-right:2px;padding:5px;';
//第二種優(yōu)化方法
var el = document.getElementById('mydiv');
el.className='active';
批量修改DOM
可以通過(guò)如下步驟減少重繪和重排次數(shù):
- 使元素脫離文檔流
- 對(duì)其應(yīng)用多重改變
- 把元素待會(huì)文檔中
這樣操作后只會(huì)在1和3補(bǔ)執(zhí)行兩次重排,忽略了步驟2中可能的N次重排滤蝠。
使元素脫離文檔流的方法有如下三種: - 隱藏元素豌熄,應(yīng)用修改,重新顯示
- 使用文檔片段在當(dāng)前DOM之外構(gòu)建一個(gè)子樹(shù)物咳,執(zhí)行完修改后再把它拷貝回文檔
- 講原始元素拷貝到一個(gè)脫離文檔的節(jié)點(diǎn)中锣险,修改這個(gè)副本,完成后再替換原始元素览闰。
function appendDataToElement(appendToElement, data) {
var a, li;
for (var i = 0, max = data.length; i < max; i++) {
a = document.createElement('a');
a.href = data[i].url;
a.appendChild(document.createTextNode(data[i].name));
li = document.createElement('li');
li.appendChild(a);
appendToElement.appendChild(li);
}
};
//優(yōu)化前,循環(huán)內(nèi)N次重排
var ul = document.getElementById('mylist');
appendDataToElement(ul, data);
//第一種芯肤,異常和顯示
var ul = document.getElementById('mylist');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';
//第二種,文檔片段
var fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
document.getElementById('mylist').appendChild(fragment);
//第三種压鉴,元素替換
var old = document.getElementById('mylist');
var clone = old.cloneNode(true);
appendDataToElement(clone, data);
old.parentNode.replaceChild(clone, old);
推薦第二種方案崖咨,其產(chǎn)生的DOM遍歷和重排次數(shù)最少。
緩存布局信息
當(dāng)查詢布局信息(例如offsets,scroll,client等)油吭,瀏覽器為返回最新值击蹲,會(huì)刷新隊(duì)列并應(yīng)用所有變更。所喲盡量減少布局信息的獲取次數(shù)上鞠,獲取后賦值給局部變量际邻,然后再操作局部變量。
例如移動(dòng)元素的例子芍阎,timeout循環(huán)部分
//改進(jìn)前
myElement.style.left = 1 + myElement.offsetLeft + 'px';
myElement.style.top = 1 + myElement.offsetTop + 'px';
if (myElement.offsetLeft >= 500) {
stopAnimation();
}
//改進(jìn)后世曾,先一次性獲取初始位置
var current = myElement.offsetLeft;
//然后循環(huán)執(zhí)行操作
current++
myElement.style.left = current + 'px';
myElement.style.top = current + 'px';
if (current >= 500) {
stopAnimation();
}
讓元素脫離動(dòng)畫流
采用絕對(duì)位置定位,可以減少元素尺寸變化時(shí)谴咸,對(duì)其他元素造成的重排影響轮听。
例如折疊/展開(kāi)這種交互方式,每次變化都會(huì)導(dǎo)致下方所有元素的移動(dòng)岭佳。如果把這部分元素使用絕對(duì)位置定位血巍,覆蓋其他部分。這樣就能避免下方元素的重排和重繪珊随,減少開(kāi)銷述寡。
IE和:hover
從IE7開(kāi)始柿隙,IE允許任何元素上使用:hover這個(gè)CSS偽選擇器。如果大量使用:hover鲫凶,響應(yīng)速度下降明顯禀崖。特別是IE8。
事件委托
如果進(jìn)行大量的DOM元素事件綁定螟炫,會(huì)引入性能問(wèn)題波附。一個(gè)簡(jiǎn)單的解決方案是事件委托。只需給最外層的元素綁定事件昼钻,利用事件逐層冒泡并能被父級(jí)元素捕獲掸屡,就可以處理所有子元素上觸發(fā)的事件。