瀏覽器渲染頁面過程與頁面優化

由一道面試題引起的思考:css

從用戶輸入瀏覽器輸入url到頁面最後呈現 有哪些過程?
一道很常規的題目,考的是基本網絡原理,和瀏覽器加載css,js過程。html

答案大體以下:前端

  1. 用戶輸入URL地址html5

  2. 瀏覽器解析URL解析出主機名web

  3. 瀏覽器將主機名轉換成服務器ip地址(瀏覽器先查找本地DNS緩存列表 沒有的話 再向瀏覽器默認的DNS服務器發送查詢請求 同時緩存)面試

  4. 瀏覽器將端口號從URL中解析出來算法

  5. 瀏覽器創建一條與目標Web服務器的TCP鏈接(三次握手)chrome

  6. 瀏覽器向服務器發送一條HTTP請求報文瀏覽器

  7. 服務器向瀏覽器返回一條HTTP響應報文緩存

  8. 關閉鏈接 瀏覽器解析文檔

  9. 若是文檔中有資源 重複6 7 8 動做 直至資源所有加載完畢

以上答案基本簡述了一個網頁基本的響應過程背後的原理。
但這也只是一部分,瀏覽器獲取數據的部分,至於瀏覽器拿到數據以後,怎麼渲染頁面的,一直沒太關注。
因此抽出時間研究下瀏覽器渲染頁面的過程。
經過研究,瞭解一些基本常識的原理:

  1. 爲何要將js放到頁腳部分

  2. 引入樣式的幾種方式的權重

  3. css屬性書寫順序建議

  4. 何種類型的DOM操做是耗費性能的

瀏覽器渲染主要流程

不一樣的瀏覽器內核不一樣,因此渲染過程不太同樣。

clipboard.png

WebKit 主流程

clipboard.png

Mozilla 的 Gecko 呈現引擎主流程

由上面兩張圖能夠看出,雖然主流瀏覽器渲染過程叫法有區別,可是主要流程仍是相同的。
Gecko 將視覺格式化元素組成的樹稱爲「框架樹」。每一個元素都是一個框架。WebKit 使用的術語是「呈現樹」,它由「呈現對象」組成。對於元素的放置,WebKit 使用的術語是「佈局」,而 Gecko 稱之爲「重排」。對於鏈接 DOM 節點和可視化信息從而建立呈現樹的過程,WebKit 使用的術語是「附加」。

因此能夠分析出基本過程:

  1. HTML解析出DOM Tree

  2. CSS解析出Style Rules

  3. 將兩者關聯生成Render Tree

  4. Layout 根據Render Tree計算每一個節點的信息

  5. Painting 根據計算好的信息繪製整個頁面

HTML解析

HTML Parser的任務是將HTML標記解析成DOM Tree
這個解析能夠參考React解析DOM的過程,
可是這裏面有不少別的規則和操做,好比容錯機制,識別</br><br>等等。
感興趣的能夠參考 《How Browser Work》中文翻譯
舉個例子:一段HTML

<html>
<head>
    <title>Web page parsing</title>
</head>
<body>
    <div>
        <h1>Web page parsing</h1>
        <p>This is an example Web page.</p>
    </div>
</body>
</html>

通過解析以後的DOM Tree差很少就是

clipboard.png

將文本的HTML文檔,提煉出關鍵信息,嵌套層級的樹形結構,便於計算拓展。這就是HTML Parser的做用。

CSS解析

CSS Parser將CSS解析成Style Rules,Style Rules也叫CSSOM(CSS Object Model)。
StyleRules也是一個樹形結構,根據CSS文件整理出來的相似DOM Tree的樹形結構:

clipboard.png

於HTML Parser類似,CSS Parser做用就是將不少個CSS文件中的樣式合併解析出具備樹形結構Style Rules。

腳本處理

瀏覽器解析文檔,當遇到<script>標籤的時候,會當即解析腳本,中止解析文檔(由於JS可能會改動DOM和CSS,因此繼續解析會形成浪費)。
若是腳本是外部的,會等待腳本下載完畢,再繼續解析文檔。如今能夠在script標籤上增長屬性 defer或者async
腳本解析會將腳本中改變DOM和CSS的地方分別解析出來,追加到DOM Tree和Style Rules上。

呈現樹(Render Tree)

Render Tree的構建其實就是DOM Tree和CSSOM Attach的過程。
呈現器是和 DOM 元素相對應的,但並不是一一對應。Render Tree實際上就是一個計算好樣式,與HTML對應的(包括哪些顯示,那些不顯示)的Tree。

在 WebKit 中,解析樣式和建立呈現器的過程稱爲「附加」。每一個 DOM 節點都有一個「attach」方法。附加是同步進行的,將節點插入 DOM 樹須要調用新的節點「attach」方法。

clipboard.png

樣式計算

樣式計算是個很複雜的問題。DOM中的一個元素能夠對應樣式表中的多個元素。樣式表包括了全部樣式:瀏覽器默認樣式表,自定義樣式表,inline樣式元素,HTML可視化屬性如:width=100。後者將轉化以匹配CSS樣式。

WebKit 節點會引用樣式對象 (RenderStyle)。這些對象在某些狀況下能夠由不一樣節點共享。這些節點是同級關係,而且:

  1. 這些元素必須處於相同的鼠標狀態(例如,不容許其中一個是「:hover」狀態,而另外一個不是)

  2. 任何元素都沒有 ID

  3. 標記名稱應匹配

  4. 類屬性應匹配

  5. 映射屬性的集合必須是徹底相同的

  6. 連接狀態必須匹配

  7. 焦點狀態必須匹配

  8. 任何元素都不該受屬性選擇器的影響,這裏所說的「影響」是指在選擇器中的任何位置有任何使用了屬性選擇器的選擇器匹配

  9. 元素中不能有任何 inline 樣式屬性

  10. 不能使用任何同級選擇器。WebCore 在遇到任何同級選擇器時,只會引起一個全局開關,並停用整個文檔的樣式共享(若是存在)。這包括 + 選擇器以及 :first-child 和 :last-child 等選擇器。

爲了簡化樣式計算,Firefox 還採用了另外兩種樹:規則樹和樣式上下文樹。WebKit 也有樣式對象,但它們不是保存在相似樣式上下文樹這樣的樹結構中,只是由 DOM 節點指向此類對象的相關樣式。

clipboard.png

樣式上下文包含端值。要計算出這些值,應按照正確順序應用全部的匹配規則,並將其從邏輯值轉化爲具體的值。
例如,若是邏輯值是屏幕大小的百分比,則須要換算成絕對的單位。規則樹的點子真的很巧妙,它使得節點之間能夠共享這些值,以免重複計算,還能夠節約空間。
全部匹配的規則都存儲在樹中。路徑中的底層節點擁有較高的優先級。規則樹包含了全部已知規則匹配的路徑。規則的存儲是延遲進行的。規則樹不會在開始的時候就爲全部的節點進行計算,而是隻有當某個節點樣式須要進行計算時,纔會向規則樹添加計算的路徑。

舉個例子 咱們有段HTML代碼:

<html>
  <body>
    <div class="err" id="div1">
      <p>
        this is a <span class="big"> big error </span>
        this is also a
        <span class="big"> very  big  error</span> error
      </p>
    </div>
    <div class="err" id="div2">another error</div>
  </body>
</html>

對應CSS規則以下:

1. .div {margin:5px;color:black}
2. .err {color:red}
3. .big {margin-top:3px}
4. div span {margin-bottom:4px}
5. #div1 {color:blue}
6. #div2 {color:green}

則CSS造成的規則樹以下圖所示(節點的標記方式爲「節點名 : 指向的規則序號」)

clipboard.png

假設咱們解析 HTML 時遇到了第二個 <div> 標記,咱們須要爲此節點建立樣式上下文,並填充其樣式結構。
通過規則匹配,咱們發現該 <div> 的匹配規則是第 一、2 和 6 條。這意味着規則樹中已有一條路徑可供咱們的元素使用,咱們只須要再爲其添加一個節點以匹配第 6 條規則(規則樹中的 F 節點)。
咱們將建立樣式上下文並將其放入上下文樹中。新的樣式上下文將指向規則樹中的 F 節點。

如今咱們須要填充樣式結構。首先要填充的是 margin 結構。因爲最後的規則節點 (F) 並無添加到 margin 結構,咱們須要上溯規則樹,直至找到在先前節點插入中計算過的緩存結構,而後使用該結構。咱們會在指定 margin 規則的最上層節點(即 B 節點)上找到該結構。

咱們已經有了 color 結構的定義,所以不能使用緩存的結構。因爲 color 有一個屬性,咱們無需上溯規則樹以填充其餘屬性。咱們將計算端值(將字符串轉化爲 RGB 等)並在此節點上緩存通過計算的結構。

第二個 <span> 元素處理起來更加簡單。咱們將匹配規則,最終發現它和以前的 span 同樣指向規則 G。因爲咱們找到了指向同一節點的同級,就能夠共享整個樣式上下文了,只需指向以前 span 的上下文便可。

對於包含了繼承自父代的規則的結構,緩存是在上下文樹中進行的(事實上 color 屬性是繼承的,可是 Firefox 將其視爲 reset 屬性,並緩存到規則樹上)
因此生成的上下文樹以下:

clipboard.png

以正確的層疊順序應用規則

樣式對象具備與每一個可視化屬性一一對應的屬性(均爲 CSS 屬性但更爲通用)。若是某個屬性未由任何匹配規則所定義,那麼部分屬性就可由父代元素樣式對象繼承。其餘屬性具備默認值。
若是定義不止一個,就會出現問題,須要經過層疊順序來解決。

一些例子:

*             {}  /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */
 li            {}  /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */
 li:first-line {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
 ul li         {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
 ul ol+li      {}  /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */
 h1 + *[rel=up]{}  /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */
 ul ol li.red  {}  /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */
 li.red.level  {}  /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */
 #x34y         {}  /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */
 style=""          /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */

利用上面的方法,基本能夠快速肯定不一樣選擇器的優先級。

佈局Layout

建立渲染樹後,下一步就是佈局(Layout),或者叫回流(reflow,relayout),這個過程就是經過渲染樹中渲染對象的信息,計算出每個渲染對象的位置和尺寸,將其安置在瀏覽器窗口的正確位置,而有些時候咱們會在文檔佈局完成後對DOM進行修改,這時候可能須要從新進行佈局,也可稱其爲迴流,本質上仍是一個佈局的過程,每個渲染對象都有一個佈局或者回流方法,實現其佈局或迴流。

對渲染樹的佈局能夠分爲全局和局部的,全局即對整個渲染樹進行從新佈局,如當咱們改變了窗口尺寸或方向或者是修改了根元素的尺寸或者字體大小等;而局部佈局能夠是對渲染樹的某部分或某一個渲染對象進行從新佈局。

大多數web應用對DOM的操做都是比較頻繁,這意味着常常須要對DOM進行佈局和迴流,而若是僅僅是一些小改變,就觸發整個渲染樹的迴流,這顯然是很差的,爲了不這種狀況,瀏覽器使用了髒位系統,只有一個渲染對象改變了或者某渲染對象及其子渲染對象髒位值爲」dirty」時,說明須要迴流。

表示須要佈局的髒位值有兩種:

  • 「dirty」–自身改變,須要迴流

  • 「children are dirty」–子節點改變,須要迴流

佈局是一個從上到下,從外到內進行的遞歸過程,從根渲染對象,即對應着HTML文檔根元素,而後下一級渲染對象,如對應着元素,如此層層遞歸,依次計算每個渲染對象的幾何信息(位置和尺寸)。

每個渲染對象的佈局流程基本如:

  • 1.計算此渲染對象的寬度(width);

  • 2.遍歷此渲染對象的全部子級,依次:

    • 2.1設置子級渲染對象的座標

    • 2.2判斷是否須要觸發子渲染對象的佈局或迴流方法,計算子渲染對象的高度(height)

  • 3.設置此渲染對象的高度:根據子渲染對象的累積高,margin和padding的高度設置其高度;

  • 4.設置此渲染對象髒位值爲false。

繪製(Painting)

在繪製階段,系統會遍歷呈現樹,並調用呈現器的「paint」方法,將呈現器的內容顯示在屏幕上。繪製工做是使用用戶界面基礎組件完成的。

CSS2 規範定義了繪製流程的順序。繪製的順序其實就是元素進入堆棧樣式上下文的順序。這些堆棧會從後往前繪製,所以這樣的順序會影響繪製。塊呈現器的堆棧順序以下:

  1. 背景顏色

  2. 背景圖片

  3. 邊框

  4. 子代

  5. 輪廓

這裏還要說兩個概念,一個是Reflow,另外一個是Repaint。這兩個不是一回事。
Repaint ——屏幕的一部分要重畫,好比某個CSS的背景色變了。可是元素的幾何尺寸沒有變。
Reflow 元件的幾何尺寸變了,咱們須要從新驗證並計算Render Tree。是Render Tree的一部分或所有發生了變化。這就是Reflow,或是Layout。(HTML使用的是flow based layout,也就是流式佈局,因此,若是某元件的幾何尺寸發生了變化,須要從新佈局,也就叫reflow)reflow 會從<html>這個root frame開始遞歸往下,依次計算全部的結點幾何尺寸和位置,在reflow過程當中,可能會增長一些frame,好比一個文本字符串必需被包裝起來。

Reflow的成本比Repaint的成本高得多的多。DOM Tree裏的每一個結點都會有reflow方法,一個結點的reflow頗有可能致使子結點,甚至父點以及同級結點的reflow。在一些高性能的電腦上也許還沒什麼,可是若是reflow發生在手機上,那麼這個過程是很是痛苦和耗電的。 因此,下面這些動做有很大可能會是成本比較高的。

  • 當你增長、刪除、修改DOM結點時,會致使Reflow或Repaint

  • 當你移動DOM的位置,或是搞個動畫的時候。

  • 當你修改CSS樣式的時候。

  • 當你Resize窗口的時候(移動端沒有這個問題),或是滾動的時候。

  • 當你修改網頁的默認字體時。

  • 注:display:none會觸發reflow,而visibility:hidden只會觸發repaint,由於沒有發現位置變化。

基本上來講,reflow有以下的幾個緣由:

  • Initial。網頁初始化的時候。

  • Incremental。一些Javascript在操做DOM Tree時。

  • Resize。其些元件的尺寸變了。

  • StyleChange。若是CSS的屬性發生變化了。

  • Dirty。幾個Incremental的reflow發生在同一個frame的子樹上。

看幾個例子:

$('body').css('color', 'red'); // repaint
$('body').css('margin', '2px'); // reflow, repaint

var bstyle = document.body.style; // cache

bstyle.padding = "20px"; // reflow, repaint
bstyle.border = "10px solid red"; //  再一次的 reflow 和 repaint

bstyle.color = "blue"; // repaint
bstyle.backgroundColor = "#fad"; // repaint

bstyle.fontSize = "2em"; // reflow, repaint

// new DOM element - reflow, repaint
document.body.appendChild(document.createTextNode('dude!'));

固然,咱們的瀏覽器是聰明的,它不會像上面那樣,你每改一次樣式,它就reflow或repaint一次。通常來講,瀏覽器會把這樣的操做積攢一批,而後作一次reflow,這又叫異步reflow或增量異步reflow。可是有些狀況瀏覽器是不會這麼作的,好比:resize窗口,改變了頁面默認的字體,等。對於這些操做,瀏覽器會立刻進行reflow。

可是有些時候,咱們的腳本會阻止瀏覽器這麼幹,好比:若是咱們請求下面的一些DOM值:

offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop/Left/Width/Height
clientTop/Left/Width/Height
IE中的 getComputedStyle(), 或 currentStyle

由於,若是咱們的程序須要這些值,那麼瀏覽器須要返回最新的值,而這樣同樣會flush出去一些樣式的改變,從而形成頻繁的reflow/repaint。

Chrome調試工具查看頁面渲染順序

頁面的渲染詳細過程能夠經過chrome開發者工具中的timeline查看

clipboard.png

  1. 發起請求;

  2. 解析HTML;

  3. 解析樣式;

  4. 執行JavaScript;

  5. 佈局;

  6. 繪製

頁面渲染優化

瀏覽器對上文介紹的關鍵渲染路徑進行了不少優化,針對每一次變化產生儘可能少的操做,還有優化判斷從新繪製或佈局的方式等等。
在改變文檔根元素的字體顏色等視覺性信息時,會觸發整個文檔的重繪,而改變某元素的字體顏色則只觸發特定元素的重繪;改變元素的位置信息會同時觸發此元素(可能還包括其兄弟元素或子級元素)的佈局和重繪。某些重大改變,如更改文檔根元素的字體尺寸,則會觸發整個文檔的從新佈局和重繪,據此及上文所述,推薦如下優化和實踐:

  1. HTML文檔結構層次儘可能少,最好不深於六層;

  2. 腳本儘可能後放,放在前便可;

  3. 少許首屏樣式內聯放在標籤內;

  4. 樣式結構層次儘可能簡單;

  5. 在腳本中儘可能減小DOM操做,儘可能緩存訪問DOM的樣式信息,避免過分觸發迴流;

  6. 減小經過JavaScript代碼修改元素樣式,儘可能使用修改class名方式操做樣式或動畫;

  7. 動畫儘可能使用在絕對定位或固定定位的元素上;

  8. 隱藏在屏幕外,或在頁面滾動時,儘可能中止動畫;

  9. 儘可能緩存DOM查找,查找器儘可能簡潔;

  10. 涉及多域名的網站,能夠開啓域名預解析

總結

瀏覽器渲染是個很繁瑣的過程,其中每一步都有對應的算法。
瞭解渲染過程原理能夠有針對的性能優化,並且也能夠懂得一些基本的要求和規範的原理。
最後文章中間不少語句都是直接複製的原文,本身的語言概況仍是不及原文精彩。

參考連接

  1. 《How Browser Work》

  2. 瀏覽器的工做原理:新式網絡瀏覽器幕後揭祕

  3. 瀏覽器渲染原理

  4. 淺析前端頁面渲染機制

  5. 瀏覽器 渲染,繪製流程及性能優化

  6. 優化CSS重排重繪與瀏覽器性能

相關文章
相關標籤/搜索