基礎知識
瀏覽器下載完頁面中的所有組件--HTML標記灾部、JS、CSS惯退、圖片--之后會解析并生成兩個內部數(shù)據(jù)結構:
- DOM 樹:表示頁面結構
- 渲染樹:表示DOM節(jié)點如何顯示
網(wǎng)頁生成的過程
- HTML被HTML解析器解析成DOM 樹
- css被css解析器解析成CSSOM(CSS Object Model)
- attachment DOM 樹和CSSOM赌髓,生成渲染樹(Render Tree)
- 生成布局(flow),即將所有渲染樹的所有節(jié)點進行平面合成
- 將布局繪制(paint)在屏幕上
"生成布局"(flow)和"繪制"(paint)這兩步催跪,合稱為"渲染"(render)锁蠕。
網(wǎng)頁生成的時候,至少會渲染一次懊蒸。用戶訪問的過程中荣倾,還會不斷重新渲染。
節(jié)點定義
DOM 樹種的每一個需要顯示的節(jié)點在渲染樹中至少存在一個對應的節(jié)點(隱藏的DOM元素在渲染樹中沒有對應的節(jié)點)骑丸。
渲染樹中的節(jié)點稱為“幀(frames)”或“盒(boxes)”舌仍,符合CSS模型的定義鳖孤。
重排和重繪
定義
重排是什么:重新生成布局。當DOM 的變化影響了元素的幾何屬性(寬和高)--比如改變邊框寬度或給段落增加文字導致行數(shù)增加--瀏覽器需要重新計算元素的幾何屬性抡笼,同樣其他元素的幾何屬性和位置也會因此受到影響苏揣。瀏覽器會使渲染樹中受到影響的部分失效,并重新構造渲染樹推姻。這個過程稱為重排平匈。
重繪是什么:重新繪制。完成重排后藏古,瀏覽器會重新繪制受影響的部分到屏幕中增炭。這個過程稱為重繪。
重排與重繪的關系
重排一定會導致重繪拧晕,重繪不一定導致重排隙姿。如果DOM變化不影響幾何屬性,元素的布局沒有改變厂捞,則只發(fā)生一次重繪(不需要重排)输玷。
發(fā)生重排的情況
當頁面布局和幾何屬性改變時發(fā)生“重排”。如下:
- 添加或刪除可見的DOM 元素
- 元素位置改變
- 元素尺寸改變(包括外邊距靡馁、內邊距欲鹏、邊框厚度、寬度臭墨、高度等屬性改變)
- 內容改變赔嚎,例如:文本改變后圖片被另一個不同尺寸的圖片替代
- 頁面渲染器初始化
- 瀏覽器窗口尺寸改變
發(fā)生重排的范圍
整個頁面或局部。例如:當滾動條出現(xiàn)時觸發(fā)整個頁面的重排胧弛。
對性能的影響
重排和重繪會不斷觸發(fā)尤误,這是不可避免的。但是结缚,它們非常耗費資源损晤,是導致網(wǎng)頁性能低下的根本原因。
提高網(wǎng)頁性能掺冠,就是要降低"重排"和"重繪"的頻率和成本沉馆,盡量少觸發(fā)重新渲染码党。
渲染樹變化的排隊
前面提到德崭,DOM變動和樣式變動,都會觸發(fā)重新渲染揖盘。但是眉厨,瀏覽器已經(jīng)很智能了,會盡量把所有的變動集中在一起兽狭,排成一個隊列憾股,然后一次性執(zhí)行鹿蜀,盡量避免多次重新渲染。
div.style.color = 'blue';
div.style.marginTop = '30px';
上面代碼中服球,div元素有兩個樣式變動茴恰,但是瀏覽器只會觸發(fā)一次重排和重繪。
如果寫得不好斩熊,就會觸發(fā)兩次重排和重繪往枣。
div.style.color = 'blue';
var margin = parseInt(div.style.marginTop);
div.style.marginTop = (margin + 10) + 'px';
上面代碼對div元素設置背景色以后,第二行要求瀏覽器給出該元素的位置粉渠,所以瀏覽器不得不立即重排分冈。
強制刷新隊列
獲取布局信息的操作會導致列隊刷新,以下屬性和方法需要返回最新的布局信息霸株,最好避免使用雕沉。
offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop, scrollLeft, scrollWidth, scrollHeight
clientTop, clientLeft, clientWidth, clientHeight
getComputedStyle() (currentStyle in IE)
clientTop:元素上邊框的厚度,當沒有指定邊框厚底時去件,一般為0坡椒。
scrollTop:位于對象最頂端和窗口中可見內容的最頂端之間的距離,簡單地說就是滾動后被隱藏的高度尤溜。
offsetTop:獲取對象相對于由offsetParent屬性指定的父坐標(css定位的元素或body元素)距離頂端的高度肠牲。
clientHeight:內容可視區(qū)域的高度,也就是說頁面瀏覽器中可以看到內容的這個區(qū)域的高度靴跛,一般是最后一個工具條以下到狀態(tài)欄以上的這個區(qū)域缀雳,與頁面內容無關。
scrollHeight:IE梢睛、Opera 認為 scrollHeight 是網(wǎng)頁內容實際高度肥印,可以小于 clientHeight。FF 認為 scrollHeight 是網(wǎng)頁內容高度绝葡,不過最小值是 clientHeight深碱。
offsetHeight:獲取對象相對于由offsetParent屬性指定的父坐標(css定位的元素或body元素)的高度。IE藏畅、Opera 認為 offsetHeight = clientHeight + 滾動條 + 邊框敷硅。FF 認為 offsetHeight 是網(wǎng)頁內容實際高度,可以小于clientHeight愉阎。offsetHeight在新版本的FF和IE中是一樣的绞蹦,表示網(wǎng)頁的高度,與滾動條無關榜旦,chrome中不包括滾動條幽七。
Window.getComputedStyle()方法返回一個對象,該對象在應用活動樣式表并解析這些值可能包含的任何基本計算后報告元素的所有CSS屬性的值溅呢。 私有的CSS屬性值可以通過對象提供的API或通過簡單地使用CSS屬性名稱進行索引來訪問澡屡。
解決辦法
所以猿挚,從性能角度考慮,盡量不要把讀操作和寫操作驶鹉,放在一個語句里面绩蜻。
// 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ī)則是:
- 樣式表越簡單,重排和重繪就越快室埋。
- 重排和重繪的DOM元素層級越高辜羊,成本就越高。
- table元素的重排和重繪成本词顾,要高于div元素八秃。
瀏覽器優(yōu)化方法
1. 減少布局信息的獲取次數(shù),獲取后賦值給局部變量肉盹,操作局部變量
當查詢布局信息時昔驱,比如獲取偏移量(offset)、滾動位置(scroll)或計算出的樣式值(computedstyle values)時上忍,瀏覽器為了返回最新值骤肛,會刷新隊列并應用所有變更。不利于優(yōu)化窍蓝。
所以應該盡量減少布局信息的獲取次數(shù)腋颠,獲取后把它賦值給局部變量,然后再操作局部變量
// 優(yōu)化前
myElement.style.left = 1 + myElement.offsetLeft + 'px';
myElement.style.top = 1 + myElement.offsetTop + 'px';
if (myElement.offsetLeft >= 500) {
stopAnimation();
}
// 優(yōu)化后
// 獲取一次起始位置的值吓笙,然后賦值給一個變量淑玫,在動畫循環(huán)中直接使用變量不再查詢偏移量
var current = myElement.offsetLeft;
current++;
myElement.style.left = current + 'px';
myElement.style.top = current + 'px';
if (myElement.offsetLeft >= 500) {
stopAnimation();
}
2. 合并多次對DOM 和樣式的修改:使用cssText屬性
現(xiàn)在大部分瀏覽器都自動優(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. 合并樣式的修改時:修改css的class名稱而不是修改內聯(lián)樣式
var el = document.getElementById('mydiv');
el.className = "active";
4. 使元素脫離文檔流、對其改變后再把元素帶回文檔中
var ul = document.getElementById('mylist');
ul.style.display = 'none';
appendDataToElement(ul, data); // 更新指定節(jié)點數(shù)據(jù)的函數(shù)
ul.style.display = 'block';
5. (推薦使用)在文檔之外創(chuàng)建并更新一個文檔片段面睛,然后把它附加到原始列表中
文檔片段是個輕量級的document對象絮蒿,用于更新和移動節(jié)點。當你附加一個片段到節(jié)點中叁鉴,實際上添加的是該片段的子節(jié)點土涝,而不是片段本身。
該方法產(chǎn)生的DOM遍歷和重排次數(shù)最少幌墓。
//創(chuàng)建一個文檔片段
var fragment = document.createDocumentFragment();
// 更新文檔片段的數(shù)據(jù)
appendDataToElement(fragment, data);
// 將文檔片段附加到原始列表中(實際添加的是子節(jié)點)
document.getElementById('mylist').appendChild(fragment);
實例如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>使用fragment進行重排重繪</title>
</head>
<body>
<ul id="myList">
<li>a</li>
<li>b</li>
</ul>
<p>
向上面的ul中加入兩個新的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. 備份一個節(jié)點,對副本操作常侣,完成后用副本節(jié)點代替舊節(jié)點
var old = document.getElementById('mylist');
// 對舊節(jié)點備份
var clone = old.cloneNode(true);
appendDataToElement(clone, data);
// 用副本節(jié)點代替舊節(jié)點
old.parentNode.replaceChild(clone, old);
7. 讓元素脫離動畫流
許多展開區(qū)域的幾何動畫會將頁面其他部分推向下方蜡饵。一般來說,重排只影響渲染樹中的一部分袭祟,但是也可能影響很大的部分验残。
當頁面頂部的一個動畫推移頁面整個余下的部分時,會導致一次代價昂貴的大規(guī)模重排巾乳。
使用以下步驟可以避免頁面中的大部分重排:
- 使用絕對位置定位頁面上的動畫元素您没,將其脫離文檔流
- 讓元素動起來。當它擴大時胆绊,會臨時覆蓋部分頁面氨鹏。但這只是頁面一個小區(qū)域的重繪過程,不會產(chǎn)生重排并重繪頁面的大部分內容压状。
- 當動畫結束時恢復定位仆抵,從而只會下移一次文檔的其他元素。