重繪與重排及它的性能優化


1.重繪與重排

瀏覽器下載完頁面中的所有組件——HTML標記、JavaScript、CSS、圖片之后會解析生成兩個內部數據結構——DOM樹和渲染樹。

DOM樹表示頁面結構,渲染樹表示DOM節點如何顯示。DOM樹中的每一個需要顯示的節點在渲染樹種至少存在一個對應的節點(隱藏的DOM元素disply值為none 在渲染樹中沒有對應的節點)。渲染樹中的節點被稱為“幀”或“盒”,符合CSS模型的定義,理解頁面元素為一個具有填充,邊距,邊框和位置的盒子。一旦DOM和渲染樹構建完成,瀏覽器就開始顯示(繪制)頁面元素。

當DOM的變化影響了元素的幾何屬性(寬或高),瀏覽器需要重新計算元素的幾何屬性,同樣其他元素的幾何屬性和位置也會因此受到影響。瀏覽器會使渲染樹中受到影響的部分失效,並重新構造渲染樹。這個過程稱為重排。完成重排后,瀏覽器會重新繪制受影響的部分到屏幕,該過程稱為重繪。由於瀏覽器的流布局,對渲染樹的計算通常只需要遍歷一次就可以完成。但table及其內部元素除外,它可能需要多次計算才能確定好其在渲染樹中節點的屬性,通常要花3倍於同等元素的時間。這也是為什么我們要避免使用table做布局的一個原因。

並不是所有的DOM變化都會影響幾何屬性,比如改變一個元素的背景色並不會影響元素的寬和高,這種情況下只會發生重繪。

不管頁面發生了重繪還是重排,它們都會影響性能(重繪還好一些) 
能避免要盡量避免

 

2觸發重排

頁面布局和元素幾何屬性的改變就會導致重排 
下列情況會發生重排

  • 頁面初始渲染
  • 添加/刪除可見DOM元素
  • 改變元素位置
  • 改變元素尺寸(寬、高、內外邊距、邊框等)
  • 改變元素內容(文本或圖片等)
  • 改變窗口尺寸

不同的條件下發生重排的范圍及程度會不同 
某些情況甚至會重排整個頁面,比如滑動滾動條

 

3.瀏覽器的優化:渲染隊列

舉個小例子 
比如我們想用js中修改一個div元素的樣式 
寫下了以下代碼

div.style.left = '10px';

div.style.top = '10px';

div.style.width = '20px';

div.style.height = '20px';

我們修改了元素的left、top、width、height屬性 
滿足我們發生重排的條件 
理論上會發生4次重排 
但是實際上只會發生1次重排 
這是因為我們現代的瀏覽器都有渲染隊列的機制 
當我改變了元素的一個樣式會導致瀏覽器發生重排或重繪時 
它會進入一個渲染隊列 
然后瀏覽器繼續往下看,如果下面還有樣式修改 
那么同樣入隊 
直到下面沒有樣式修改 
瀏覽器會按照渲染隊列批量執行來優化重排過程,一並修改樣式 
這樣就把本該4次的重排優化為1次

但是我們現在想要修改樣式后在控制台打印

div.style.left = '10px';

console.log(div.offsetLeft);

div.style.top = '10px';

console.log(div.offsetTop);

div.style.width = '20px';

console.log(div.offsetWidth);

div.style.height = '20px';

console.log(div.offsetHeight);

千萬不要寫這樣的代碼,因為發生了4次重排 
有同學可能不懂了,不是說瀏覽器有渲染隊列優化機制嗎? 
為什么這樣寫就會發生4次重排 
因為offsetLeft/Top/Width/Height非常叼 
它們會強制刷新隊列要求樣式修改任務立刻執行 
想一想其實這么做是有道理的 
畢竟瀏覽器不確定在接下來的代碼中你是否還會修改同樣的樣式 
為了保證獲得正確的值,它不得不立刻執行渲染隊列觸發重排(錯的不是我,是這個世界)

以下屬性或方法會刷新渲染隊列

  • offsetTop、offsetLeft、offsetWidth、offsetHeight
  • clientTop、clientLeft、clientWidth、clientHeight
  • scrollTop、scrollLeft、scrollWidth、scrollHeight
  • getComputedStyle()(IE中currentStyle)

我們在修改樣式過程中,要盡量避免使用上面的屬性

 

4重繪與重排的性能優化

A 分離讀寫操作

了解了原理我們就可以對上面的代碼進行優化

div.style.left = '10px';

div.style.top = '10px';

div.style.width = '20px';

div.style.height = '20px';

console.log(div.offsetLeft);

console.log(div.offsetTop);

console.log(div.offsetWidth);

console.log(div.offsetHeight);

這樣就僅僅發生1次重排了,原因相信大家已經很清晰了 
把所有的讀操作移到所有寫操作之后 
效率高多了 
這是其中一種優化的方法

 

B 樣式集中改變

還是我們最初修改樣式的代碼

div.style.left = '10px';

div.style.top = '10px';

div.style.width = '20px';

div.style.height = '20px';

雖然現代瀏覽器有渲染隊列的優化機制 
但是古董瀏覽器效率仍然底下,觸發了4次重排 
即便這樣,我們仍然可以做出優化 
我們需要cssText屬性合並所有樣式改變

div.style.cssText = 'left:10px;top:10px;width:20px;height:20px;';

這樣只需要修改DOM一次一並處理 
僅僅觸發了1次重排 
而且只用了一行代碼,看起來相對干凈一些

不過有一點要注意,cssText會覆蓋已有的行間樣式 
如果想保留原有行間樣式,這樣做

div.style.cssText += ';left:10px;';

 

除了cssText以外,我們還可以通過修改class類名來進行樣式修改

div.className = 'new-class';

這種辦法可維護性好,還可以幫助我們免除顯示性代碼 
(有一點點性能影響,改變class需要檢查級聯樣式,不過瑕不掩瑜)

 

C 緩存布局信息

我覺得緩存真是萬金油,哪種性能優化都少不了它

div.style.left = div.offsetLeft + 1 + 'px';

div.style.top = div.offsetTop + 1 + 'px';

這種讀操作完就執行寫操作造成了2次重排 
緩存可以進行優化

var curLeft = div.offsetLeft;

var curTop = div.offsetTop;

div.style.left = curLeft + 1 + 'px';

div.style.top = curTop + 1 + 'px';

這也相當於是分離讀寫操作了 
優化為1次重排

 

D元素批量修改

現在我們想要向ul中循環添加大量li 
(如果ul還不存在,最好的辦法是先循環添加li到ul,然后再把ul添加到文檔,1次重排)

var ul = document.getElementById('demo');

for(var i = 0; i < 1e5; i++){

    var li = document.createElement('li');

    var text = document.createTextNode(i);

    li.appendChild(text);

    ul.appendChild(li);

}

我可以做出下面的優化

var ul = document.getElementById('demo');

ul.style.display = 'none'; <--

for(var i = 0; i < 1e5; i++){

    var li = document.createElement('li');

    var text = document.createTextNode(i);

    li.appendChild(text);

    ul.appendChild(li);

}

ul.style.display = 'block'; <--

var ul = document.getElementById('demo');

var frg = document.createDocumentFragment(); <--

for(var i = 0; i < 1e5; i++){

    var li = document.createElement('li');

    var text = document.createTextNode(i);

    li.appendChild(text);

    frg.appendChild(li); <--

}

ul.appendChild(frg); <--

var ul = document.getElementById('demo');

var clone = ul.cloneNode(true); <--

for(var i = 0; i < 1e5; i++){

    var li = document.createElement('li');

    var text = document.createTextNode(i);

    li.appendChild(text);

    clone.appendChild(li); <--

}

ul.parentNode.replaceChild(clone,ul); <--

上面的方法減少重繪和重排的原理很簡單

  • 元素脫離文檔
  • 改變樣式
  • 元素回歸文檔

而改變元素就分別使用了隱藏元素、文檔碎片和克隆元素 
上面的方法我認為僅僅是理論上可以優化重排重繪次數 
現代瀏覽器的優化可能會超過我們的想象

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM