【高效能JS】重繪、重排與瀏覽器優化方法

wdapp發表於2020-03-12

基礎知識

瀏覽器下載完頁面中的所有元件--HTML標記、JS、CSS、圖片--之後會解析並生成兩個內部資料結構:

  • DOM 樹:表示頁面結構
  • 渲染樹:表示DOM節點如何顯示

網頁生成的過程

  1. HTML被HTML解析器解析成DOM 樹
  2. css被css解析器解析成CSSOM(CSS Object Model)
  3. attachment DOM 樹和CSSOM,生成渲染樹(Render Tree)
  4. 生成佈局(flow),即將所有渲染樹的所有節點進行平面合成
  5. 將佈局繪製(paint)在螢幕上

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

網頁生成的時候,至少會渲染一次。使用者訪問的過程中,還會不斷重新渲染。

【高效能JS】重繪、重排與瀏覽器優化方法

節點定義

DOM 樹種的每一個需要顯示的節點在渲染樹中至少存在一個對應的節點(隱藏的DOM元素在渲染樹中沒有對應的節點)。

渲染樹中的節點稱為“幀(frames)”或“盒(boxes)”,符合CSS模型的定義。

重排和重繪

定義

  • 重排是什麼:重新生成佈局。當DOM 的變化影響了元素的幾何屬性(寬和高)--比如改變邊框寬度或給段落增加文字導致行數增加--瀏覽器需要重新計算元素的幾何屬性,同樣其他元素的幾何屬性和位置也會因此受到影響。瀏覽器會使渲染樹中受到影響的部分失效,並重新構造渲染樹。這個過程稱為重排。

  • 重繪是什麼:重新繪製。完成重排後,瀏覽器會重新繪製受影響的部分到螢幕中。這個過程稱為重繪。

重排與重繪的關係

重排一定會導致重繪,重繪不一定導致重排。如果DOM變化不影響幾何屬性,元素的佈局沒有改變,則只發生一次重繪(不需要重排)。

發生重排的情況

當頁面佈局和幾何屬性改變時發生“重排”。如下:

  • 新增或刪除可見的DOM 元素
  • 元素位置改變
  • 元素尺寸改變(包括外邊距、內邊距、邊框厚度、寬度、高度等屬性改變)
  • 內容改變,例如:文字改變後圖片被另一個不同尺寸的圖片替代
  • 頁面渲染器初始化
  • 瀏覽器視窗尺寸改變

發生重排的範圍

整個頁面或區域性。例如:當滾動條出現時觸發整個頁面的重排。

對效能的影響

重排和重繪會不斷觸發,這是不可避免的。但是,它們非常耗費資源,是導致網頁效能低下的根本原因。

提高網頁效能,就是要降低"重排"和"重繪"的頻率和成本,儘量少觸發重新渲染。

渲染樹變化的排隊

前面提到,DOM變動和樣式變動,都會觸發重新渲染。但是,瀏覽器已經很智慧了,會盡量把所有的變動集中在一起,排成一個佇列,然後一次性執行,儘量避免多次重新渲染。

div.style.color = 'blue';
div.style.marginTop = '30px';
複製程式碼

上面程式碼中,div元素有兩個樣式變動,但是瀏覽器只會觸發一次重排和重繪。

如果寫得不好,就會觸發兩次重排和重繪。

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:內容可視區域的高度,也就是說頁面瀏覽器中可以看到內容的這個區域的高度,一般是最後一個工具條以下到狀態列以上的這個區域,與頁面內容無關。

scrollHeight:IE、Opera 認為 scrollHeight 是網頁內容實際高度,可以小於 clientHeight。FF 認為 scrollHeight 是網頁內容高度,不過最小值是 clientHeight。

offsetHeight:獲取物件相對於由offsetParent屬性指定的父座標(css定位的元素或body元素)的高度。IE、Opera 認為 offsetHeight = clientHeight + 滾動條 + 邊框。FF 認為 offsetHeight 是網頁內容實際高度,可以小於clientHeight。offsetHeight在新版本的FF和IE中是一樣的,表示網頁的高度,與滾動條無關,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";

複製程式碼

一般的規則是:

  • 樣式表越簡單,重排和重繪就越快。
  • 重排和重繪的DOM元素層級越高,成本就越高。
  • table元素的重排和重繪成本,要高於div元素。

瀏覽器優化方法

1. 減少佈局資訊的獲取次數,獲取後賦值給區域性變數,操作區域性變數 當查詢佈局資訊時,比如獲取偏移量(offset)、滾動位置(scroll)或計算出的樣式值(computedstyle values)時,瀏覽器為了返回最新值,會重新整理佇列並應用所有變更。不利於優化。 所以應該儘量減少佈局資訊的獲取次數,獲取後把它賦值給區域性變數,然後再操作區域性變數

// 優化前
myElement.style.left = 1 + myElement.offsetLeft + 'px';
myElement.style.top = 1 + myElement.offsetTop + 'px';
if (myElement.offsetLeft >= 500) {
    stopAnimation();
}

// 優化後
// 獲取一次起始位置的值,然後賦值給一個變數,在動畫迴圈中直接使用變數不再查詢偏移量
var current = myElement.offsetLeft;
current++;
myElement.style.left = current + 'px';
myElement.style.top = current + 'px';
if (myElement.offsetLeft >= 500) {
    stopAnimation();
}

複製程式碼

2. 合併多次對DOM 和樣式的修改:使用cssText屬性

現在大部分瀏覽器都自動優化了

// 優化前
 var el = document.getElementById('mydiv');
 el.style.borderLeft = '1px';
 el.style.borderRight = '2px';
 el.style.padding = '5px';
 
 // 優化後
 var el = document.getElementById('mydiv');
 el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;';

複製程式碼

3. 合併樣式的修改時:修改css的class名稱而不是修改內聯樣式

 var el = document.getElementById('mydiv');
 el.className = "active";

複製程式碼

4. 使元素脫離文件流、對其改變後再把元素帶回文件中

var ul = document.getElementById('mylist');
ul.style.display = 'none';
appendDataToElement(ul, data); // 更新指定節點資料的函式
ul.style.display = 'block';

複製程式碼

5. (推薦使用)在文件之外建立並更新一個文件片段,然後把它附加到原始列表中

文件片段是個輕量級的document物件,用於更新和移動節點。當你附加一個片段到節點中,實際上新增的是該片段的子節點,而不是片段本身。

該方法產生的DOM遍歷和重排次數最少。

//建立一個文件片段
var fragment = document.createDocumentFragment();

// 更新文件片段的資料
appendDataToElement(fragment, data);

// 將文件片段附加到原始列表中(實際新增的是子節點)
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. 備份一個節點,對副本操作,完成後用副本節點代替舊節點

var old = document.getElementById('mylist');

// 對舊節點備份
var clone = old.cloneNode(true);

appendDataToElement(clone, data);

// 用副本節點代替舊節點
old.parentNode.replaceChild(clone, old);

複製程式碼

7. 讓元素脫離動畫流 許多展開區域的幾何動畫會將頁面其他部分推向下方。一般來說,重排隻影響渲染樹中的一部分,但是也可能影響很大的部分。 當頁面頂部的一個動畫推移頁面整個餘下的部分時,會導致一次代價昂貴的大規模重排。 使用以下步驟可以避免頁面中的大部分重排:

  1. 使用絕對位置定位頁面上的動畫元素,將其脫離文件流
  2. 讓元素動起來。當它擴大時,會臨時覆蓋部分頁面。但這只是頁面一個小區域的重繪過程,不會產生重排並重繪頁面的大部分內容。
  3. 當動畫結束時恢復定位,從而只會下移一次文件的其他元素。

相關文章