【翻譯】瀏覽器渲染Rendering那些事:repaint、reflow/relayout、restyle


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

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

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

一、瀏覽器渲染過程

不同的瀏覽器的渲染過程存在些許不同,但大體的機制是一樣的,下圖展示的是瀏覽器自下載完全部的代碼后的大致流程

  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到屏幕上。

 二、森林和樹

首先我們先看一個例子:

<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)。

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

三、重繪-repaint和回流-reflow

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

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

重繪和回流的性能消耗是非常嚴重的,破壞用戶體驗,造成UI卡頓。

四、觸發重繪/回流的機制

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

  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做動畫或改變尺寸,頁面的其他部分會被擠來擠去,這聽起來會消耗很多性能。

五、聰明的瀏覽器

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

但是,有時候某些的代碼會破壞上述的瀏覽器優化機制,導致瀏覽器刷新緩存隊列並且執行所有已已緩存的操作行為。這種情況發生在請求/獲取下面這些樣式的行為中:

  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";

六、如何減少重繪和回流

減少因為重繪和回流引起的糟糕用戶體驗的本質是盡量減少重繪和回流,減少樣式信息的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. 總之,當你在打算改變樣式時,首先考慮一下渲染樹的機制,並且評估一下你的操作會引發多少刷新渲染樹的行為。例如,我們知道一個絕對定位的節點是會脫離文檔流,所以當對此節點應用動畫時不會對其他節點產生很大影響,當絕對定位的節點置於其他節點上層時,其他節點只會觸發重繪,而不會觸發回流。

七、工具

(廢話就不翻譯了,大概就是一些吐槽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?

八、舉個栗子

 下面我們簡單的看一個如何運用工具來證明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倍。(呵呵)

九、總結

非常感謝各位對這篇文章的支持。希望各位能通過運動上文提到的測試工具改善工作,並且時刻注意回流的觸發操作。最后,我們復習一下幾個術語:

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

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


免責聲明!

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



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