[轉載] 瀏覽器渲染Rendering那些事:repaint、reflow/relayout、restyle

原文連接:http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/javascript

轉載連接:http://www.cnblogs.com/ihardcoder/p/3927709.htmlphp

我的備註:css

  1. 渲染樹的部分或者所有將須要從新構造而且渲染節點的大小須要從新計算。這個過程叫作迴流-reflow,或者layout。瀏覽器中至少存在一個reflow行爲-即頁面的初始化layout。
  2. 將從新計算後的渲染樹更新到屏幕的行爲叫作重繪-repaint,或者redraw。要麼是由於節點的幾何結構改變,要麼是由於格式改變,如背景色的變化。
  3. restyle——沒有幾何結構改變的渲染樹變化(而回流reflow會同時影響佈局layout)。html

  4. 改變任何影響構造渲染樹的行爲都會觸發重繪repaint,例如java

    1. 增長、刪除、更新DOM節點;
    2. 經過display:none隱藏節點會觸發重繪和迴流,經過visibility:hidden隱藏只會觸發重繪,由於沒有幾何結構的改變;
    3. 移動節點和動畫;
    4. 增長、調整樣式;
    5. 用戶操做行爲,如調整窗口大小、改變字體大小、滾動窗口等。
  5. 如何減小重繪repaint和迴流reflownode

    1. 不要逐個修改多個樣式。對於靜態樣式來講,最明智和易維護的代碼是經過改變classname來控制樣式;而對於動態樣式來講,經過一次修改節點的cssText來代替樣式的逐個改變。
    2. "離線"處理多個DOM操做。「離線」的意思是將須要進行的DOM操做脫離DOM樹,好比:
      • 經過documentFragment集中處理臨時操做;
      • 將須要更新的節點克隆,在克隆節點上進行更新操做,而後把原始節點替換爲克隆節點;
      • 先經過設置display:none將節點隱藏(此時出發一次迴流和重繪),而後對隱藏的節點進行100個操做(這些操做都會單獨觸發迴流和重繪),完畢後將節點的display改回原值(此時再次觸發一次迴流和重繪)。經過這種方法,將100次迴流和重繪縮減爲2次,大大減小了消耗
    3. 不要過多進行重複的樣式計算操做。若是你須要重複利用一個靜態樣式值,能夠只計算一次,用一個局部變量儲存,而後利用這個局部變量進行相關操做。
    4. 總之,當你在打算改變樣式時,首先考慮一下渲染樹的機制,而且評估一下你的操做會引起多少刷新渲染樹的行爲。例如,咱們知道一個絕對定位的節點是會脫離文檔流,因此當對此節點應用動畫時不會對其餘節點產生很大影響,當絕對定位的節點置於其餘節點上層時,其餘節點只會觸發重繪,而不會觸發迴流。

 

有沒有被標題中的5個「R」嚇到?今天,咱們來討論一下瀏覽器的渲染(Rendering)-一個產生於Page 2.0生命週期中,甚至有時候會在下載瀑布流中出現的過程。web

咱們來討論瀏覽器在接收到HTML、CSS和JavasSript後,如何把你的頁面呈如今屏幕上。瀏覽器

1、瀏覽器渲染過程

不一樣的瀏覽器的渲染過程存在些許不一樣,但大致的機制是同樣的,下圖展現的是瀏覽器自下載徹底部的代碼後的大體流程緩存

  1. 首先,瀏覽器解析HTML源碼構建DOM樹,在DOM樹中,每一個HTML標籤都有對應的節點,而且在介於兩個標籤中間的文字塊也對應一個text節點。DOM樹的根節點是documentElement,也就是<html>標籤;
  2. 而後,瀏覽器對CSS代碼進行解析,一些當前瀏覽器不能識別的CSS hack寫法(如-moz-/-webkit等前綴,以及IE下的*/_等)將會被忽略。CSS樣式的優先級以下:最低的是瀏覽器的默認樣式,而後是經過<link>、import引入的外部樣式和行內樣式,最高級的是直接寫在標籤的style屬性中的樣式;
  3. 隨後將進入很是有趣的環節-構建渲染樹。渲染樹跟DOM樹結構類似但並不徹底匹配。渲染樹會識別樣式,因此若是經過設置display:none隱藏的標籤是不會被渲染樹引入的。一樣的規則適用於<head>標籤以及其包含的全部內容。另外,在渲染樹中可能存在多個渲染節點(渲染樹中的節點稱爲渲染節點)映射爲一個DOM標籤,例如,多行文字的<p>標籤中的每一行文字都會被視爲一個單獨的渲染節點。渲染樹的一個節點也稱爲frame-結構體,或者盒子-box(與CSS盒子相似)。每一個渲染節點都具備CSS盒子的屬性,如width、height、border、margin等;
  4. 最後,等待渲染樹構建完畢後,瀏覽器便開始將渲染節點一一繪製-paint到屏幕上。

 2、森林和樹

首先咱們先看一個例子:app

複製代碼
<html>
<head>
  <title>Beautiful page</title>
</head>
<body>
    
  <p>
    Once upon a time there was 
    a looong paragraph...
  </p>
  
  <div style="display: none">
    Secret message
  </div>
  
  <div><img src="..." /></div>
  ...
 
</body>
</html>
複製代碼

HTML結構中的每一個標籤和標籤間的文字都會被映射爲DOM樹種的一個節點(實際上,空白區域也會被映射爲一個text節點,爲了簡單說明,在此忽略),構建完成的DOM樹結構以下:

複製代碼
documentElement (html)
    head
        title
    body
        p
            [text node]
        
        div 
            [text node]
        
        div
            img
        
        ...
複製代碼

因爲渲染樹會忽略head內容和隱藏的節點,而且會將<p>中的多行文字按行數映射爲單獨的渲染節點,故構建完成的渲染樹結構以下:

複製代碼
root (RenderView)
    body
        p
            line 1
        line 2
        line 3
        ...
        
    div
        img
        
    ...
複製代碼

渲染樹的根節點是一個包括全部其餘節點的結構體(盒子)。你能夠將它理解爲瀏覽器窗口的內部區域(我的理解爲可繪製區域,即不包括瀏覽器邊框、菜單欄、標籤欄等等),頁面被限制在此區域內。嚴格來講,webkit將渲染樹的根節點稱爲渲染視圖-RenderView,渲染視圖符合CSS初始包含塊-initial containing block,也就是瀏覽器的整個可繪製區域,從座標(0,0)到(window.innerWidth,window.innerHeight)。

接下來,咱們將研究瀏覽器是如何經過循環遍歷渲染樹把頁面展現到屏幕上的。

3、重繪-repaint和迴流-reflow

同一時間內至少存在一個頁面初始化layout行爲和一個繪製行爲(除非你的頁面是空白頁-blank)。在此以後,改變任何影響構造渲染樹的行爲都會觸發如下一種或者多種動做:

  1. 渲染樹的部分或者所有將須要從新構造而且渲染節點的大小須要從新計算。這個過程叫作迴流-reflow,或者layout,或者layouting(靠,能不能愉快的翻譯了,是否是還來個過去式啊?!),或者relayout(這詞是原文做者杜撰的,爲了標題中多個「R」)。瀏覽器中至少存在一個reflow行爲-即頁面的初始化layout;
  2. 屏幕的部分區域須要進行更新,要麼是由於節點的幾何結構改變,要麼是由於格式改變,如背景色的變化。屏幕的更新行爲稱做重繪-repaint,或者redraw。

重繪和迴流的性能消耗是很是嚴重的,破壞用戶體驗,形成UI卡頓。

4、觸發重繪/迴流的機制

改變任何影響構造渲染樹的行爲都會觸發重繪,例如

  1. 增長、刪除、更新DOM節點;
  2. 經過display:none隱藏節點會觸發重繪和迴流,經過visibility:hidden隱藏只會觸發重繪,由於沒有幾何結構的改變;
  3. 移動節點和動畫;
  4. 增長、調整樣式;
  5. 用戶操做行爲,如調整窗口大小、改變字體大小、滾動窗口(OMG,no!)等。

舉個栗子:

複製代碼
var bstyle = document.body.style; // 緩存
 
bstyle.padding = "20px"; // 觸發重繪和迴流
bstyle.border = "10px solid red"; // 再次觸發重繪和迴流
 
bstyle.color = "blue"; // 只觸發重繪,由於幾何結構沒有改變
bstyle.backgroundColor = "#fad"; // 同上
 
bstyle.fontSize = "2em"; // 再再次觸發重繪和迴流
 
// 新增DOM節點,再再再次觸發重繪和迴流
document.body.appendChild(document.createTextNode('dude!'));
複製代碼

有些迴流行爲要比其餘的花銷大一些。設想以下情景,一個直屬於body節點的渲染樹,若是你在此渲染樹中亂搞,它不會影響不少其餘節點(這個長句翻譯很差,原文以下:Think of the render tree - if you fiddle with a node way down the tree that is a direct descendant of the body, then you're probably not invalidating a lot of other nodes)。可是若是將頁面頂部的一個div作動畫或改變尺寸,頁面的其餘部分會被擠來擠去,這聽起來會消耗不少性能。

5、聰明的瀏覽器

瀏覽器一直在努力減小消耗巨大的重繪和迴流行爲。要麼選擇不執行,要麼至少不當即執行。瀏覽器會生成一個隊列用於緩存這些行爲而且以塊爲單位執行它們。經過這種方法,屢次引起重繪或迴流的操做會被組合在一塊兒,以便在一個迴流中完成。瀏覽器將這些操做加入到緩存隊列中,當到達必定的時間間隔,或者累積了足夠多的操做行爲後執行它們。

可是,有時候某些的代碼會破壞上述的瀏覽器優化機制,致使瀏覽器刷新緩存隊列而且執行全部已已緩存的操做行爲。這種狀況發生在請求/獲取下面這些樣式的行爲中:

  1. offsetTop,offsetLeft,offsetWidth,offsetheight
  2. scrollTop/Left/Width/Height
  3. clientTop/Left/Width/Height
  4. getComputedStyle(),或者IE下的currentStyle

以上的行爲本質上是獲取一個節點的樣式信息,瀏覽器必須提供最新的值。爲了達到此目的,瀏覽器須要將緩存隊列中的全部行爲所有執行完畢,而且被強制迴流。

因此,在一條邏輯中同時執行set和get樣式操做時很是很差的,以下:

el.style.left = el.offsetLeft + 10 + "px";

6、如何減小重繪和迴流

減小由於重繪和迴流引發的糟糕用戶體驗的本質是儘可能減小重繪和迴流,減小樣式信息的set行爲。能夠經過如下幾點來優化:

  1. 不要逐個修改多個樣式。對於靜態樣式來講,最明智和易維護的代碼是經過改變classname來控制樣式;而對於動態樣式來講,經過一次修改節點的cssText來代替樣式的逐個改變。
    複製代碼
    // 糟糕的辦法
    var left = 10,
        top = 10;
    el.style.left = left + "px";
    el.style.top  = top  + "px";
     
    //靜態樣式經過改變classname
    // better 
    el.className += " theclassname";
     
    // 動態樣式統一修改cssText
    // better
    el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
    複製代碼
  2. "離線"處理多個DOM操做。「離線」的意思是將須要進行的DOM操做脫離DOM樹,好比:
    • 經過documentFragment集中處理臨時操做;
    • 將須要更新的節點克隆,在克隆節點上進行更新操做,而後把原始節點替換爲克隆節點;
    • 先經過設置display:none將節點隱藏(此時出發一次迴流和重繪),而後對隱藏的節點進行100個操做(這些操做都會單獨觸發迴流和重繪),完畢後將節點的display改回原值(此時再次觸發一次迴流和重繪)。經過這種方法,將100次迴流和重繪縮減爲2次,大大減小了消耗
  3. 不要過多進行重複的樣式計算操做。若是你須要重複利用一個靜態樣式值,能夠只計算一次,用一個局部變量儲存,而後利用這個局部變量進行相關操做。例如:
    複製代碼
    //糟糕的作法
    for(big; loop; here) {
        el.style.left = el.offsetLeft + 10 + "px";
        el.style.top  = el.offsetTop  + 10 + "px";
    }
     
    //優化後的代碼
    var left = el.offsetLeft,
        top  = el.offsetTop
        esty = el.style;
    for(big; loop; here) {
        left += 10;
        top  += 10;
        esty.left = left + "px";
        esty.top  = top  + "px";
    }
    複製代碼
  4. 總之,當你在打算改變樣式時,首先考慮一下渲染樹的機制,而且評估一下你的操做會引起多少刷新渲染樹的行爲。例如,咱們知道一個絕對定位的節點是會脫離文檔流,因此當對此節點應用動畫時不會對其餘節點產生很大影響,當絕對定位的節點置於其餘節點上層時,其餘節點只會觸發重繪,而不會觸發迴流。

7、工具

(廢話就不翻譯了,大概就是一些吐槽IE開發者工具的話)

如今(原文做於2009年12月)有不少能夠幫助咱們深刻了解瀏覽器重繪和迴流機制的工具。

  • FireFox提供了mozAfterPaint接口可供開發者查看重繪的動做;
  • DynaTrace Ajax適用於IE瀏覽器,谷歌的SpeedTracer適用於Webkit內核的瀏覽器,這兩種工具能夠幫助開發者深刻挖掘重繪和迴流行爲;

Douglas Crockford去年提到,咱們可能會對一些不太瞭解的CSS作一些愚蠢的事情,而且我被包括在內。我被引入了一個項目組,研究一種奇怪的現象:在IE6瀏覽器中增大font-size會引發CPU佔用率到達100%,而且會持續10到15分鐘,IE瀏覽器纔會完成重繪行爲。

有了工具的輔助,咱們沒有任何理由再作一些愚蠢的CSS操做了。

順便提一句,若是有一種像Firebug的工具能夠象查看DOM結構同樣查看渲染樹,是否是很cooooooooooooooool?

8、舉個栗子

 下面咱們簡單的看一個如何運用工具來證實restyle(沒有幾何結構改變的渲染樹變化)和迴流(同時影響佈局layout)、重繪。

第一個測試,咱們比較解決同一問題的兩種方法。第一種方法,改變一些樣式,在每次改變以後檢查一次唄改變的樣式。

複製代碼
bodystyle.color = 'red';
tmp = computed.backgroundColor;
bodystyle.color = 'white';
tmp = computed.backgroundImage;
bodystyle.color = 'green';
tmp = computed.backgroundAttachment;
複製代碼

 

第二種方法,在等待所有樣式改變完畢後再檢查變化的樣式信息。

複製代碼
bodystyle.color = 'yellow';
bodystyle.color = 'pink';
bodystyle.color = 'blue';
 
tmp = computed.backgroundColor;
tmp = computed.backgroundImage;
tmp = computed.backgroundAttachment;
複製代碼

 

上面兩種方法用到的幾個變量以下:

複製代碼
var bodystyle = document.body.style;
var computed;
if (document.body.currentStyle) {
  computed = document.body.currentStyle;
} else {
  computed = document.defaultView.getComputedStyle(document.body, '');
}
複製代碼

 

上面兩中方法的樣式改變經過click事件觸發。測試頁面-restyle.html(點擊「dude」)。咱們將第一個測試稱爲restyle測試。

第二個測試在第一個測試的基礎上,同事改變影響佈局的樣式。

複製代碼
// 每次修改後都檢查
bodystyle.color = 'red';
bodystyle.padding = '1px';
tmp = computed.backgroundColor;
bodystyle.color = 'white';
bodystyle.padding = '2px';
tmp = computed.backgroundImage;
bodystyle.color = 'green';
bodystyle.padding = '3px';
tmp = computed.backgroundAttachment;
 
 
// 所有修改完畢後再檢查
bodystyle.color = 'yellow';
bodystyle.padding = '4px';
bodystyle.color = 'pink';
bodystyle.padding = '5px';
bodystyle.color = 'blue';
bodystyle.padding = '6px';
tmp = computed.backgroundColor;
tmp = computed.backgroundImage;
tmp = computed.backgroundAttachment;
複製代碼

 

咱們稱第二個測試爲relayout測試,測試頁面請點擊

咱們經過DynaTrace工具獲得restyle測試的表現以下圖:

等頁面加載完畢後,在第2秒左右點擊觸發第一種方案(即每次修改樣式後當即檢查),而後在第4秒左右再次點擊觸發第二種方案(即等待全部樣式修改完畢後再統一檢查)。

 DynaTrace工具會顯示頁面的加載過程,從上圖能夠看到IE的logo圖標被加載的時間節點。把鼠標移至Rendering一行以便追蹤點擊事件,滑動滾輪放大想要追蹤的區域能夠查看詳細信息,以下圖:

從上圖中能夠清晰的看到表明JavaScript行爲的藍色柱形條,一屆表明渲染行爲的綠色柱形條。經過這個簡單的實驗,咱們能夠注意到兩個柱形條的長度,也就是比較渲染行爲比JavaScript行爲多花費的時間。在Ajax以及富應用中,性能瓶頸並非JavaScript行爲,而是DOM節點的操做使用和渲染行爲。

接下來咱們來運行relayout測試,也就是涉及幾何結構改變的操做行爲。經過測試工具的「PurePaths」視圖,查看每種行爲執行時間的瀑布流。下圖中高亮部分顯示的是第一次點擊事件,執行一段JavaScript邏輯實現一些layout操做。

以下圖所示,咱們能夠看到在此次的測試中,除了與第一次測試一樣的具備表明「繪圖」的綠色柱形條之外,還有一個新增的區域-「計算佈局流」,由於此次測試中同時觸發了重繪和迴流。

接下來,咱們經過SpeedTracer工具在Chrome下運行上面兩個測試。

第一個測試-restyle測試的運行結果以下圖所示:

總的來講,仍然是一次點擊觸發一次重繪,可是咱們注意到,在第一次點擊的時候,會有50%的時間消耗在計算樣式(Style Recalculation)上。致使這種結果的緣由是咱們在每次改變樣式後都檢查了一次樣式信息。

展開事件詳細信息後能夠清晰的看到,在第一次點擊事件後,樣式被計算了3次。而第二次點擊值計算了一次。以下圖所示:

接下來運行第二個測試-relayout測試。整體事件信息與restyle測試大體相同:

可是詳情頁顯示的信息能夠看到第一次點擊後觸發了3次迴流(由請求樣式信息操做觸發),第二次點擊只觸發了一次迴流。經過本工具能夠清晰的看到瀏覽器內部到底發生了什麼。

上述兩種工具的區別在於:DynaTrace會顯示layout行爲被執行和加入執行隊列的詳細時間,而SpeedTracer不會;SpeedTracer會將restyle與reflow/layout兩種瀏覽器行爲區別開,而DynaTrace不會。難道IE瀏覽器自己不會區分這兩種行爲?另外,在兩種不一樣的邏輯測試-改變-最後檢查(change-end-touch)與改變-當即檢查(change-then-touch)中,DynaTrace並不會顯示二者觸發迴流的次數不一樣(第一種之觸發一次,第二次觸發3次,而DynaTrace統一顯示爲一次),難道IE瀏覽器的工做機制本就如此?

即便運行上述測試幾百次,IE瀏覽器仍然不關心你在改變樣式後是否請求樣式信息。(譯者注:我彷佛感到原文做者對IE滿滿的惡意...)

在屢次運行上述測試後,獲得幾點結論以下:

  1. Chrome中,相比較改變樣式後當即檢查樣式信息,等待所有樣式修改完畢後統一檢查,在restyle測試中會快2.5倍,relayout測試中快4.42倍;
  2. Firefox中,restyle測試快1.87倍,relayout測試快4.64倍;
  3. IE6和IE8,不要在乎這些細節(呵呵)

在全部瀏覽器(IE系列不在「全部」的範疇)的測試結果顯示,只修改樣式的時間花銷僅僅是同時改變樣式和觸發layout的一半(我本該對比只改變樣式和只改變layout的時間的,可是我沒有,不用謝)。順便提一下IE6,它的layout時間花銷是隻改變樣式的4倍。(呵呵)

9、總結

很是感謝各位對這篇文章的支持。但願各位能經過運動上文提到的測試工具改善工做,而且時刻注意迴流的觸發操做。最後,咱們複習一下幾個術語:

  1. 渲染樹-DOM樹的虛擬部分;
  2. 渲染樹中的節點稱爲結構體或者盒子;
  3. 從新計算渲染樹的行爲被Mozilla稱爲迴流-reflow,被其餘瀏覽器稱爲layout;
  4. 將從新計算後的渲染樹更新到屏幕的行爲叫作重繪-repaint,或者redraw(in IE/DynaTrace);
  5. SpeedTracer會將「計算樣式-style recalculation」和「佈局-layout」區分開。

擴展閱讀,前三篇對瀏覽器內部機制研究比較深刻,推薦:

相關文章
相關標籤/搜索