【高性能JS】重繪、重排與瀏覽器優(yōu)化方法

基礎(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ò)程

  1. HTML被HTML解析器解析成DOM 樹(shù)
  2. css被css解析器解析成CSSOM(CSS Object Model)
  3. attachment DOM 樹(shù)和CSSOM,生成渲染樹(shù)(Render Tree)
  4. 生成布局(flow),即將所有渲染樹(shù)的所有節(jié)點(diǎn)進(jìn)行平面合成
  5. 將布局繪制(paint)在屏幕上

"生成布局"(flow)和"繪制"(paint)這兩步,合稱為"渲染"(render)。

網(wǎng)頁(yè)生成的時(shí)候,至少會(huì)渲染一次。用戶訪問(wèn)的過(guò)程中,還會(huì)不斷重新渲染。

image

節(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è)面中的大部分重排:

  1. 使用絕對(duì)位置定位頁(yè)面上的動(dòng)畫元素,將其脫離文檔流
  2. 讓元素動(dòng)起來(lái)。當(dāng)它擴(kuò)大時(shí),會(huì)臨時(shí)覆蓋部分頁(yè)面。但這只是頁(yè)面一個(gè)小區(qū)域的重繪過(guò)程,不會(huì)產(chǎn)生重排并重繪頁(yè)面的大部分內(nèi)容。
  3. 當(dāng)動(dòng)畫結(jié)束時(shí)恢復(fù)定位,從而只會(huì)下移一次文檔的其他元素。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容