基礎(chǔ)知識(shí)
瀏覽器下載完頁(yè)面中的所有組件--HTML標(biāo)記、JS、CSS、圖片--之后會(huì)解析并生成兩個(gè)內(nèi)部數(shù)據(jù)結(jié)構(gòu):
- DOM 樹(shù):表示頁(yè)面結(jié)構(gòu)
- 渲染樹(shù):表示DOM節(jié)點(diǎn)如何顯示
網(wǎng)頁(yè)生成的過(guò)程
- HTML被HTML解析器解析成DOM 樹(shù)
- css被css解析器解析成CSSOM(CSS Object Model)
- attachment DOM 樹(shù)和CSSOM,生成渲染樹(shù)(Render Tree)
- 生成布局(flow),即將所有渲染樹(shù)的所有節(jié)點(diǎn)進(jìn)行平面合成
- 將布局繪制(paint)在屏幕上
"生成布局"(flow)和"繪制"(paint)這兩步,合稱為"渲染"(render)。
網(wǎng)頁(yè)生成的時(shí)候,至少會(huì)渲染一次。用戶訪問(wèn)的過(guò)程中,還會(huì)不斷重新渲染。

節(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)稱為“幀(frames)”或“盒(boxes)”,符合CSS模型的定義。
重排和重繪
定義
重排是什么:重新生成布局。當(dāng)DOM 的變化影響了元素的幾何屬性(寬和高)--比如改變邊框?qū)挾然蚪o段落增加文字導(dǎo)致行數(shù)增加--瀏覽器需要重新計(jì)算元素的幾何屬性,同樣其他元素的幾何屬性和位置也會(huì)因此受到影響。瀏覽器會(huì)使渲染樹(shù)中受到影響的部分失效,并重新構(gòu)造渲染樹(shù)。這個(gè)過(guò)程稱為重排。
重繪是什么:重新繪制。完成重排后,瀏覽器會(huì)重新繪制受影響的部分到屏幕中。這個(gè)過(guò)程稱為重繪。
重排與重繪的關(guān)系
重排一定會(huì)導(dǎo)致重繪,重繪不一定導(dǎo)致重排。如果DOM變化不影響幾何屬性,元素的布局沒(méi)有改變,則只發(fā)生一次重繪(不需要重排)。
發(fā)生重排的情況
當(dāng)頁(yè)面布局和幾何屬性改變時(shí)發(fā)生“重排”。如下:
- 添加或刪除可見(jiàn)的DOM 元素
- 元素位置改變
- 元素尺寸改變(包括外邊距、內(nèi)邊距、邊框厚度、寬度、高度等屬性改變)
- 內(nèi)容改變,例如:文本改變后圖片被另一個(gè)不同尺寸的圖片替代
- 頁(yè)面渲染器初始化
- 瀏覽器窗口尺寸改變
發(fā)生重排的范圍
整個(gè)頁(yè)面或局部。例如:當(dāng)滾動(dòng)條出現(xiàn)時(shí)觸發(fā)整個(gè)頁(yè)面的重排。
對(duì)性能的影響
重排和重繪會(huì)不斷觸發(fā),這是不可避免的。但是,它們非常耗費(fèi)資源,是導(dǎo)致網(wǎng)頁(yè)性能低下的根本原因。
提高網(wǎng)頁(yè)性能,就是要降低"重排"和"重繪"的頻率和成本,盡量少觸發(fā)重新渲染。
渲染樹(shù)變化的排隊(duì)
前面提到,DOM變動(dòng)和樣式變動(dòng),都會(huì)觸發(fā)重新渲染。但是,瀏覽器已經(jīng)很智能了,會(huì)盡量把所有的變動(dòng)集中在一起,排成一個(gè)隊(duì)列,然后一次性執(zhí)行,盡量避免多次重新渲染。
div.style.color = 'blue';
div.style.marginTop = '30px';
上面代碼中,div元素有兩個(gè)樣式變動(dòng),但是瀏覽器只會(huì)觸發(fā)一次重排和重繪。
如果寫得不好,就會(huì)觸發(fā)兩次重排和重繪。
div.style.color = 'blue';
var margin = parseInt(div.style.marginTop);
div.style.marginTop = (margin + 10) + 'px';
上面代碼對(duì)div元素設(shè)置背景色以后,第二行要求瀏覽器給出該元素的位置,所以瀏覽器不得不立即重排。
強(qiáng)制刷新隊(duì)列
獲取布局信息的操作會(huì)導(dǎo)致列隊(duì)刷新,以下屬性和方法需要返回最新的布局信息,最好避免使用。
offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop, scrollLeft, scrollWidth, scrollHeight
clientTop, clientLeft, clientWidth, clientHeight
getComputedStyle() (currentStyle in IE)
clientTop:元素上邊框的厚度,當(dāng)沒(méi)有指定邊框厚底時(shí),一般為0。
scrollTop:位于對(duì)象最頂端和窗口中可見(jiàn)內(nèi)容的最頂端之間的距離,簡(jiǎn)單地說(shuō)就是滾動(dòng)后被隱藏的高度。
offsetTop:獲取對(duì)象相對(duì)于由offsetParent屬性指定的父坐標(biāo)(css定位的元素或body元素)距離頂端的高度。
clientHeight:內(nèi)容可視區(qū)域的高度,也就是說(shuō)頁(yè)面瀏覽器中可以看到內(nèi)容的這個(gè)區(qū)域的高度,一般是最后一個(gè)工具條以下到狀態(tài)欄以上的這個(gè)區(qū)域,與頁(yè)面內(nèi)容無(wú)關(guān)。
scrollHeight:IE、Opera 認(rèn)為 scrollHeight 是網(wǎng)頁(yè)內(nèi)容實(shí)際高度,可以小于 clientHeight。FF 認(rèn)為 scrollHeight 是網(wǎng)頁(yè)內(nèi)容高度,不過(guò)最小值是 clientHeight。
offsetHeight:獲取對(duì)象相對(duì)于由offsetParent屬性指定的父坐標(biāo)(css定位的元素或body元素)的高度。IE、Opera 認(rèn)為 offsetHeight = clientHeight + 滾動(dòng)條 + 邊框。FF 認(rèn)為 offsetHeight 是網(wǎng)頁(yè)內(nèi)容實(shí)際高度,可以小于clientHeight。offsetHeight在新版本的FF和IE中是一樣的,表示網(wǎng)頁(yè)的高度,與滾動(dòng)條無(wú)關(guān),chrome中不包括滾動(dòng)條。
Window.getComputedStyle()方法返回一個(gè)對(duì)象,該對(duì)象在應(yīng)用活動(dòng)樣式表并解析這些值可能包含的任何基本計(jì)算后報(bào)告元素的所有CSS屬性的值。 私有的CSS屬性值可以通過(guò)對(duì)象提供的API或通過(guò)簡(jiǎn)單地使用CSS屬性名稱進(jìn)行索引來(lái)訪問(wèn)。
解決辦法
所以,從性能角度考慮,盡量不要把讀操作和寫操作,放在一個(gè)語(yǔ)句里面。
// bad
div.style.left = div.offsetLeft + 10 + "px";
div.style.top = div.offsetTop + 10 + "px";
// good
var left = div.offsetLeft;
var top = div.offsetTop;
div.style.left = left + 10 + "px";
div.style.top = top + 10 + "px";
一般的規(guī)則是:
- 樣式表越簡(jiǎn)單,重排和重繪就越快。
- 重排和重繪的DOM元素層級(jí)越高,成本就越高。
- table元素的重排和重繪成本,要高于div元素。
瀏覽器優(yōu)化方法
1. 減少布局信息的獲取次數(shù),獲取后賦值給局部變量,操作局部變量
當(dāng)查詢布局信息時(shí),比如獲取偏移量(offset)、滾動(dòng)位置(scroll)或計(jì)算出的樣式值(computedstyle values)時(shí),瀏覽器為了返回最新值,會(huì)刷新隊(duì)列并應(yīng)用所有變更。不利于優(yōu)化。
所以應(yīng)該盡量減少布局信息的獲取次數(shù),獲取后把它賦值給局部變量,然后再操作局部變量
// 優(yōu)化前
myElement.style.left = 1 + myElement.offsetLeft + 'px';
myElement.style.top = 1 + myElement.offsetTop + 'px';
if (myElement.offsetLeft >= 500) {
stopAnimation();
}
// 優(yōu)化后
// 獲取一次起始位置的值,然后賦值給一個(gè)變量,在動(dòng)畫循環(huán)中直接使用變量不再查詢偏移量
var current = myElement.offsetLeft;
current++;
myElement.style.left = current + 'px';
myElement.style.top = current + 'px';
if (myElement.offsetLeft >= 500) {
stopAnimation();
}
2. 合并多次對(duì)DOM 和樣式的修改:使用cssText屬性
現(xiàn)在大部分瀏覽器都自動(dòng)優(yōu)化了
// 優(yōu)化前
var el = document.getElementById('mydiv');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';
// 優(yōu)化后
var el = document.getElementById('mydiv');
el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;';
3. 合并樣式的修改時(shí):修改css的class名稱而不是修改內(nèi)聯(lián)樣式
var el = document.getElementById('mydiv');
el.className = "active";
4. 使元素脫離文檔流、對(duì)其改變后再把元素帶回文檔中
var ul = document.getElementById('mylist');
ul.style.display = 'none';
appendDataToElement(ul, data); // 更新指定節(jié)點(diǎn)數(shù)據(jù)的函數(shù)
ul.style.display = 'block';
5. (推薦使用)在文檔之外創(chuàng)建并更新一個(gè)文檔片段,然后把它附加到原始列表中
文檔片段是個(gè)輕量級(jí)的document對(duì)象,用于更新和移動(dòng)節(jié)點(diǎn)。當(dāng)你附加一個(gè)片段到節(jié)點(diǎn)中,實(shí)際上添加的是該片段的子節(jié)點(diǎn),而不是片段本身。
該方法產(chǎn)生的DOM遍歷和重排次數(shù)最少。
//創(chuàng)建一個(gè)文檔片段
var fragment = document.createDocumentFragment();
// 更新文檔片段的數(shù)據(jù)
appendDataToElement(fragment, data);
// 將文檔片段附加到原始列表中(實(shí)際添加的是子節(jié)點(diǎn))
document.getElementById('mylist').appendChild(fragment);
實(shí)例如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>使用fragment進(jìn)行重排重繪</title>
</head>
<body>
<ul id="myList">
<li>a</li>
<li>b</li>
</ul>
<p>
向上面的ul中加入兩個(gè)新的li,比較使用fragment和不使用的性能
</p>
<script>
console.time(0);
var newLi1 = document.createElement('li');
newLi1.innerHTML = 'c';
var newLi2 = document.createElement('li');
newLi2.innerHTML = 'd';
document.getElementById('myList').appendChild(newLi1);
document.getElementById('myList').appendChild(newLi2);
console.timeEnd(0)
console.time(1);
var fragment = document.createDocumentFragment();
var newLi1 = document.createElement('li');
newLi1.innerHTML = 'c';
var newLi2 = document.createElement('li');
newLi2.innerHTML = 'd';
fragment.appendChild(newLi1);
fragment.appendChild(newLi2);
document.getElementById('myList').appendChild(fragment);
console.timeEnd(1)
</script>
</body>
</html>
6. 備份一個(gè)節(jié)點(diǎn),對(duì)副本操作,完成后用副本節(jié)點(diǎn)代替舊節(jié)點(diǎn)
var old = document.getElementById('mylist');
// 對(duì)舊節(jié)點(diǎn)備份
var clone = old.cloneNode(true);
appendDataToElement(clone, data);
// 用副本節(jié)點(diǎn)代替舊節(jié)點(diǎn)
old.parentNode.replaceChild(clone, old);
7. 讓元素脫離動(dòng)畫流
許多展開(kāi)區(qū)域的幾何動(dòng)畫會(huì)將頁(yè)面其他部分推向下方。一般來(lái)說(shuō),重排只影響渲染樹(shù)中的一部分,但是也可能影響很大的部分。
當(dāng)頁(yè)面頂部的一個(gè)動(dòng)畫推移頁(yè)面整個(gè)余下的部分時(shí),會(huì)導(dǎo)致一次代價(jià)昂貴的大規(guī)模重排。
使用以下步驟可以避免頁(yè)面中的大部分重排:
- 使用絕對(duì)位置定位頁(yè)面上的動(dòng)畫元素,將其脫離文檔流
- 讓元素動(dòng)起來(lái)。當(dāng)它擴(kuò)大時(shí),會(huì)臨時(shí)覆蓋部分頁(yè)面。但這只是頁(yè)面一個(gè)小區(qū)域的重繪過(guò)程,不會(huì)產(chǎn)生重排并重繪頁(yè)面的大部分內(nèi)容。
- 當(dāng)動(dòng)畫結(jié)束時(shí)恢復(fù)定位,從而只會(huì)下移一次文檔的其他元素。