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

本文參考: 《高性能JS》、網頁性能管理詳解javascript

基礎知識

瀏覽器下載完頁面中的全部組件--HTML標記、JS、CSS、圖片--以後會解析並生成兩個內部數據結構: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)。html

網頁生成的時候,至少會渲染一次。用戶訪問的過程當中,還會不斷從新渲染。java

節點定義

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

渲染樹中的節點稱爲「幀(frames)」或「盒(boxes)」,符合CSS模型的定義。chrome

重排和重繪

定義

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

  • 重繪是什麼:從新繪製。完成重排後,瀏覽器會從新繪製受影響的部分到屏幕中。這個過程稱爲重繪。數據結構

重排與重繪的關係

重排必定會致使重繪,重繪不必定致使重排。若是DOM變化不影響幾何屬性,元素的佈局沒有改變,則只發生一次重繪(不須要重排)。app

發生重排的狀況

當頁面佈局和幾何屬性改變時發生「重排」。以下:函數

  • 添加或刪除可見的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. 動畫結束時恢復定位,從而只會下移一次文檔的其餘元素。
相關文章
相關標籤/搜索