深刻淺出瀏覽器渲染流程

1、瀏覽器如何渲染網頁

要了解瀏覽器渲染頁面的過程,首先得知道一個名詞——關鍵路徑渲染。關鍵渲染路徑(Critical Rendering Path)是指與當前用戶操做有關的內容。例如用戶在瀏覽器中打開一個頁面,其中頁面所顯示的東西就是當前用戶操做相關的內容,也就是瀏覽器從服務器那收到的HTML,CSS,JavaScript等相關資源,而後通過一系列處理後渲染出來web頁面。實際抽象出來理解能夠將這些步驟看做一個函數,就輸入HTML,通過一層層的處理,最後輸出像素。javascript

而瀏覽器渲染的過程主要包括如下幾步:css

  • 瀏覽器將獲取的HTML文檔並解析成DOM樹。
  • 將 css 文件處理成 StyleSheet 對象,從而進行樣式計算。
  • 根據dom樹和StyleSheet 生成佈局樹。
  • 根據具體的節點信息對頁面進行分層處理,生成圖層樹
  • 根據圖層樹生成繪製列表
  • 合成線程經過主線程提交的繪製列表對圖層進行分塊,並進行柵格化,生成位圖
  • 合成位圖,並將其顯示

具體以下圖過程以下圖所示:
html

渲染流程.PNG

須要注意的是,以上幾個步驟並不必定是一次性順序完成,好比 DOM 被修改時,亦或是哪一個過程會重複執行,這樣才能計算出哪些像素須要在屏幕上進行從新渲染。而在實際狀況中,JavaScript和CSS的某些操做每每會屢次修改DOM或者CSSOM。html5

值得注意的的是,在每一個階段,都會有對應的輸入,處理,以及輸出。下面咱們就來詳細的瞭解一下這幾個過程及須要注意的事項。java

2、瀏覽器渲染網頁的具體流程

2.1 構建DOM樹

由於瀏覽器沒法直接使用HTML/SVG/XHTML,所以當瀏覽器客戶端從服務器那接受到HTML文檔後,就會遍歷文檔節點,而後對這些文檔節點經過HTML解析器進行解析,最後生成DOM樹,所生成的 DOM 樹結構和HTML標籤一一對應。須要注意的是,在這其中HTML解析器會進行諸如:標記化算法,樹構建算法等操做,其中的規範即遵循了W3C的相應規範,也都有瀏覽器引擎本身的一些特定的操做,詳情能夠翻閱這篇很是著名的文章:node

How Browsers Work: Behind the scenes of modern web browsersweb

在此階段,輸入的便是一個HTML文件,而後會有瀏覽器的HTML解析器對其進行解析,輸出樹形結構的DOM樹。值得注意的是,HTML解析器並非等整個文檔所有加載完以後纔開始解析的,而是網絡進程加載了多少數據,HTML解析器就會解析多少數據。至關與在網絡進程與渲染進程之間會在這期間創建一個數據共享的管道,網絡進程每次收到數據都會將其轉發到渲染進程,從而保證渲染進程中的HTML解析器能夠源源不斷的獲取到用於渲染的數據。這個過程能夠理解爲下方這個過程:
算法

  1. 將字節流經過分詞器轉化爲 Token
  2. 根據 Token 生成節點 node
  3. 根據生成的節點,組成 DOM 樹

每一個頁面的DOM樹,咱們也能夠直接經過在控制檯輸入document 來進行訪問:
瀏覽器

企業微信截圖_d70fd07c-795f-4117-ba6f-4daa428c5718.png

對於DOM樹,咱們須要注意如下幾點:服務器

  1. DOM 樹從內容上來看和 HTML 幾乎如出一轍,但 DOM 是保存在內存中的樹形結構,能夠經過 JavaScript 來查詢和修改。
document.getElementsByTagName("h2")[0].innerText = "Hello World"
複製代碼
  1. display:none 的元素也會在 DOM 樹中。
  2. 註釋也會在 DOM 樹中
  3. script 標籤會在 DOM 樹中
  4. DOM 樹在構建的過程當中可能會被 CSS 和 JS 的加載而執行阻塞。

此外DOM 樹在構建的過程當中可能會被 CSS 和 JS 的加載而執行阻塞,也就是咱們常說的阻塞渲染。這是由於HTML文件是經過HTML解析器轉化成 DOM 樹的,而在HTML解析器中若是遇到了 JavaScript 腳本,HTML 解析器會先執行 JavaScript 腳本,待這個腳本執行完成以後,再繼續往下解析。所以咱們常說,將script標籤放在body下面,一般就是基於這種考慮的。但爲何CSS也有可能會阻塞DOM樹的構建呢,能夠看下面一個栗子:

<html>
    <head> <style type="text/css" src = "demo.css" /> </head> <body> <p>demo</p> <script> const p = document.getElementsByTagName('p')[0] p.innerText = 'hello world' p.style.color = 'red' </script> </body> </html>
複製代碼

因爲任何script代碼都能改變HTML的結構,所以HTML每次遇到script都會中止解析,等待JavaScript腳本被執行完成以後,再進行接下來的解析,而當咱們經過 JavaScript 去進行樣式操做的時候,這個 JavaScript 腳本執行完成的前提條件就成了須要現將樣式信息肯定下來。所以在這種狀況下,HTML解析器可否繼續執行下去,以及繼續執行的時間,也須要取決與這個CSS文件給不給面子了。這也是咱們常說的,別在 JavaScript 中操做樣式的緣由。

爲了優化這種狀況,現代瀏覽器也作了一些優化,好比預解析操做。當渲染引擎接收到字節流後,會開啓一個預解析線程,用來分析 HTML文件的代碼中的JS,CSS文件,解析到相關文件的時候,預解析進行會提早下載這些資源。

對於處理這種事情,避免阻塞的產生,咱們也有如下幾點能夠注意的:

  • 在引入順序上,CSS 資源先於 JavaScript 資源。
  • JavaScript 應儘可能少的去影響 DOM 的構建。
  • 能夠將 JavaScript 腳本設置爲異步加載,經過 async 或 defer 來標記代碼

2.2 計算樣式

在構建渲染樹時,須要計算每個呈現對象的可視化的屬性值。而這個過程就被稱爲樣式計算或者計算樣式。這個過程主要是爲了 DOM 樹中每一個節點的具體樣式,大體可分爲三大步驟:

  1. 將 CSS 解析爲瀏覽器能理解的 StyleSheet
  2. 標準化樣式表中的屬性值
  3. 計算出 DOM 樹中每一個節點的具體樣式

2.2.1 將 CSS 解析爲瀏覽器能理解的 styleSheet

和html一個道理,瀏覽器也沒法直接去理解咱們所寫的那些CSS樣式,所以瀏覽器在接收到CSS文件後,會將CSS文件轉換爲瀏覽器所能理解的 StyleSheet。轉化了的 StyleSheet 咱們一樣也能夠經過控制檯來訪問:

image.png

在這個過程當中須要注意的是:

  1. CSS解析能夠與DOM解析同時進行。
  2. CSS解析與 script 的執行互斥 。
  3. 在Webkit內核中進行了script執行優化,只有在JS訪問CSS時纔會發生互斥。
  4. CSS樣式無論是來自於 link 的外部引用,仍是style標記內的CSS,亦或是元素的style屬性內嵌的CSS,都會被解析成styleSheets。

2.2.2 標準化樣式表中的屬性值

在將CSS文轉化爲瀏覽器可以理解的 styleSheet 後,就須要對期進行進行屬性值的標準化操做了。這裏的標準化的意思就是,咱們在寫css文件的時候,會寫一些語義化的屬性好比:red/bold等等。但其實這些詞對於渲染引擎來講,卻不是那麼好理解的。所以在進行計算樣式以前,瀏覽器還會這對這些不怎麼好計算的值進行標準化,將其轉化爲渲染引擎容易理解的詞,好比將red轉化成爲 rgb(255, 0, 0)等等。

2.2.3 計算出 DOM 樹中每一個節點的具體樣式

計算出 DOM 樹中每一個節點的具體樣式主要涉及的就是CSS繼承規則和層疊規則了,對於繼承規則其實比較好理解,就是,每一個DOM節點都包含的父節點的樣式。

而層疊規則也就是樣式層疊就有點麻煩了,MDN是這麼描述層疊的:

層疊是CSS的一個基本特徵,它是一個定義瞭如何合併來自多個源的屬性值的算法。它在CSS處於核心地位,CSS的全稱層疊樣式表正是強調了這一點。

層疊的具體細節在這裏也不展開講了(我本身如今還沒搞清楚。。。),你們能夠去CSS層疊看看其內部的一些規則。

在有了css繼承規則和層疊規則後,樣式計算的這個階段就會在這兩個規則的基礎上對 DOM 節點中的每一個元素計算處具體的樣式,這個階段中最終輸出的結果會保存在 ComputedStyle 中,這個一樣能夠經過控制檯進行查看:

image.png

**

2.3 佈局階段

經過前面兩個階段,咱們已經獲得了DOM樹以及DOM樹中具體每一個元素的樣式了,但對於每一個元素所處的幾何位置咱們如今仍是不知道的,所以接下來要作的就是計算出DOM樹中可見元素的幾何位置。這個過程能夠分爲兩個階段:

  1. 建立佈局樹
  2. 佈局計算

2.3.1 建立佈局樹

因爲DOM樹還包含不少不可見的元素,好比head標籤,script標籤,以及設置爲display:none的屬性,由於瀏覽器勢必不能將全部的dom樹的元素都所有拿來進行佈局計算,所以在這個階段,瀏覽器會額外構建一顆只包含可見元素的佈局樹。在構建佈局樹期間,瀏覽器大致會進行如下一些工做:

  • 遍歷DOM樹中的全部可見節點,並將這些節點加到佈局中。
  • 將全部不可見節點忽略掉

下面兩個須要注意:

  • display: none的元素不在Render Tree中
  • visibility: hidden的元素在Render Tree中

2.3.2 佈局計算

在已經獲取了全部可見元素的樹以後,就能夠計算佈局樹節點的幾何位置了。HTML是基於流的佈局方式,所以大多數狀況下,只須要進行一次遍歷即刻計算出頁面的幾何信息。一般來講,處於流靠後的元素不會影響到靠前位置元素的幾何特徵,所以在進行佈局計算的時候,一般是按從左至右,從上至下的順序遍歷文檔(只是一般而言,好比表格啥的就不是這樣)。

佈局計算是一個遞歸的過程,它從根節點出發,而後遞歸遍歷部分或全部的節點,爲每個須要計算的呈現器計算幾何信息。這個計算量無疑是龐大的,所以爲了不一些較小的更改也會觸發頁面的總體佈局計算,瀏覽器將佈局方式分爲了全局佈局和增量佈局。

  1. 全局佈局:全局佈局是指觸發了整個佈局樹的佈局計算的佈局,包括:屏幕大小改動,字體大小改動等
  2. 增量佈局:增量佈局是指當某個呈現器發生改變了,只對相應的呈現器進行佈局計算。

在執行完佈局計算後,會將佈局計算的結果寫入佈局樹中,所以這個過程能夠理解爲一種裝飾者模式,輸入輸出都是一個佈局樹,只是在這個過程當中會將佈局計算的結果給加進去。

2.4 分層

在有了佈局樹以後,瀏覽器的仍是不能直接根據佈局樹來將頁面給畫出來,由於頁面中還存在中一些特殊的效果,好比頁面滾動,z-index等。爲了可以方便的實現這些花裏胡哨的功能,渲染引擎還須要進行一個分層處理,將特定節點生成轉筒的圖層,並生成一個圖層樹(LayerTree),這個咱們也能經過瀏覽器的面板看到:

image.png

如上圖所示,瀏覽器的頁面實際上被分紅了多個圖層,這些圖層疊加在一塊兒就造成了咱們最終所看到的頁面。須要注意的是,並非佈局樹中的每個節點都會包含一個圖層,所以若是一個節點沒有所對應的圖層,那麼它就會從屬於父節點的圖層。若是一個節點須要有本身的圖層,一般須要知足如下聯合條件

  1. 擁有層疊上下文屬性的元素
  2. 須要剪裁(clip)

2.5  圖層繪製

在肯定好圖層以後,瀏覽器的渲染引擎會對圖層樹中的每一個圖層進行繪製,渲染引擎會將一個圖層的繪製拆封成不少個小的繪製指令,而後會將這些繪製指令按照必定順序組成一個待繪製列表。和佈局相同,繪製也分爲全局和增量兩種,也是爲了不部分圖層的改變而須要對整個圖層樹進行繪製。此外,CSS也對繪製順序作了規定:

  1. 背景顏色
  2. 背景圖片
  3. 邊框
  4. 子代
  5. 輪廓 

2.6  柵格化(raster)操做

這裏的柵格化是指將圖轉化爲位圖。繪製列表只是用來記錄繪製順序和繪製指令的列表,而實際繪製操做是由渲染引擎中的合成線程來完成的。實際過程是當圖層對應的繪製列表準備好以後,主線程會將繪製列表提交給合成線程。 合成線程會根據用戶所能見的窗口範圍對一些劃分,將一些大的圖層化分爲圖塊。而後合成線程會根據用戶所見範圍附近的圖塊來優先生成位圖,實際生成位圖的操做是由柵格化來執行的。圖塊是柵格化執行的最小單元,渲染進程維護了一個柵格化的線程池,全部的圖塊柵格化操做都會在這個線程池裏進行。

一般,柵格化會使用GPU進程中的GPU來進行加速,使用GPU進程生成位圖的過程叫快速柵格化,經過這個方式生成的位圖會被保存在GPU內存中。這樣作的好處就在於,當渲染進程的主線程發生阻塞的時候,合成線程以及GPU進程不會受其影響,能夠正常運行。這也是爲啥有時候主線程卡住了,但CSS動畫依然能夠風騷依舊的緣由。

2.7  合成和顯示

在全部的圖塊都被進行柵格化後,合成線程就會生成繪製圖塊的命令——「DrawQuad」,而後將該命令提交給瀏覽器進程。瀏覽器進程裏面有一個叫 viz 的組件,用來接收合成線程發過來的 DrawQuad 命令,而後根據 DrawQuad 命令,將其頁面內容繪製到內存中,最後再將內存顯示在屏幕上。

3、瀏覽器渲染網頁的那些事兒

3.1 迴流和重繪(reflow和repaint)

咱們都知道HTML默認是流式佈局的,但CSS和JS會打破這種佈局,改變DOM的外觀樣式以及大小和位置。所以咱們就須要知道兩個概念:

  • reflow(迴流):當瀏覽器發現某個部分發生了變化從而影響了佈局,這個時候就須要倒回去從新渲染,你們稱這個回退的過程叫 reflow。 常見的reflow是一些會影響頁面佈局的操做,諸如Tab,隱藏等。reflow 會從 html 這個 root frame 開始遞歸往下,依次計算全部的結點幾何尺寸和位置,以確認是渲染樹的一部分發生變化仍是整個渲染樹。reflow幾乎是沒法避免的,由於只要用戶進行交互操做,就勢必會發生頁面的一部分的從新渲染,且一般咱們也沒法預估瀏覽器到底會reflow哪一部分的代碼,由於他們會相互影響。
  • repaint(重繪): repaint則是當咱們改變某個元素的背景色、文字顏色、邊框顏色等等不影響它周圍或內部佈局的屬性時,屏幕的一部分要重畫,可是元素的幾何尺寸和位置沒有發生改變。

須要注意的是,display:none 會觸發 reflow,而visibility: hidden屬性則並不算是不可見屬性,它的語義是隱藏元素,但元素仍然佔據着佈局空間,它會被渲染成一個空框,這在咱們上面有提到過。因此visibility:hidden 只會觸發 repaint,由於沒有發生位置變化。

咱們不能避免reflow,但仍是能經過一些操做來減小回流:

  1. 用transform作形變和位移.
  2. 經過絕對位移來脫離當前層疊上下文,造成新的Render Layer。

另外有些狀況下,好比修改了元素的樣式,瀏覽器並不會馬上reflow 或 repaint 一次,而是會把這樣的操做積攢一批,而後作一次 reflow,這又叫異步 reflow 或增量異步 reflow。可是在有些狀況下,好比resize 窗口,改變了頁面默認的字體等。對於這些操做,瀏覽器會立刻進行 reflow。

3.2 幾條關於優化渲染效率的建議

結合上文和我看到的一些文章,有如下幾點能夠優化渲染效率

  1. 合法地去書寫 HTML 和 CSS ,且不要忘了文檔編碼類型。
  2. 樣式文件應當在 head 標籤中,而腳本文件在 body 結束前,這樣能夠防止阻塞的方式。
  3. 簡化並優化CSS選擇器,儘可能將嵌套層減小到最小。
  4. 儘可能減小在 JavaScript 中進行DOM操做。
  5. 修改元素樣式時,更改其class屬性是性能最高的方法。
  6. 儘可能用 transform 來作形變和位移

參考資料:

www.html5rocks.com/en/tutorial…

time.geekbang.org/column/intr…

相關文章
相關標籤/搜索