瀏覽器渲染原理

瀏覽器渲染原理

  • 瀏覽器渲染原理
    • 1. 進程和線程
    • 2. 瀏覽器中的5個進程
    • 3. HTTP 請求流程
      • 3.1 瀏覽器發送 HTTP 請求的流程
      • 3.2 服務端處理 HTTP 請求的流程
      • 3.3 為什麼很多站點第二次打開會很快
    • 4. 輸入url地址到瀏覽器顯示頁面發生瞭什麼
    • 5. 瀏覽器渲染流程
      • 5.1 構建 DOM 樹
      • 5.2 樣式計算
        • 5.2.1 把CSS轉換為瀏覽器內容理解的結構
        • 5.2.2 轉換樣式表中的屬性值,使其標準化
        • 5.2.3 計算出DOM樹中每一個節點的具體樣式
      • 5.3 佈局階段
        • 5.3.1 創建佈局樹
        • 5.3.2 佈局計算
      • 5.4 分層 (圖層樹)
      • 5.5 圖層的繪制
      • 5.6 柵格化操作
      • 5.7 合成和顯示
      • 5.8 總結
    • 6. 相關概念
      • 6.1 更新元素的幾何屬性(重排)
      • 6.2 更新元素的繪制屬性(重繪)
      • 6.3 直接合成階段
    • 7. 優化方案

瀏覽器渲染原理 1. 進程和線程

進程 : 進程是操作系統資源分配的基本單位,進程中包含線程。簡而言之,就是正在進行中的應用程序。

線程:線程是由進程所管理的。是進程內的一個獨立執行的單位,是CPU調度的最小單位。

  • 線程進程的基本單位,一個進程由一個或者多個線程組成,搞清楚這個關系之後,我們可以明確線程就是程序執行的最小單元
  • 線程和進程一樣,也是動態概念,有創建有銷毀,存在隻是暫時的,不是永久性的。
  • 進程與線程的區別在於進程在運行時擁有獨立的內存空間,也就是說每個進程所占用的內存都是獨立的
    • 例如:微信運行時,系統會給它一個運行內存。
  • 多個線程是共享內存空間的,但是每個線程的執行是相互獨立的,線程必須依賴於進程才能執行,單獨的線程是無法執行的,由進程來控制多個線程的執行,沒有進程就不存在線程。
    • 例如:我先開啟一個發送消息的線程,那麼同時還能由一個接收消息的線程。兩個線程之間完全獨立。

為瞭提升瀏覽器的穩定性和安全性,瀏覽器采取瞭多進程模型。

2. 瀏覽器中的5個進程

目前最新的Chrome進程架構圖

瀏覽器設置的時候是一個多進程模型,這樣能確保瀏覽的安全性和穩定性。如果一個頁面有問題,不影響其他頁面的運行。

  • 瀏覽器進程。主要負責界面顯示用戶交互子進程管理、同時提供存儲等功能。
  • 渲染進程。 核心任務是將HTMLCSSJavaScript轉換為用戶可以與之交互的網頁,排版引擎BlinkJavaScript引擎V8都運行在該進程中,默認情況下,Chrome為每一個Tab標簽頁創建一個渲染進程。出於安全考慮,渲染進程都是運行在沙箱模式下的。
  • **GPU進程。**GPU圖形處理器(英語:graphics processing unit,縮寫:GPU),負責3D CSS效果,網頁,Chrome ui的繪制。
  • 網絡進程。主要負責頁面的網絡資源加載,之前是作為一個模塊運行在瀏覽器進程裡面的,直至最近才獨立處理,成為單獨一個進程。
  • 插件進程。主要負責插件的運行,因為插件易崩潰,所以通過插件進程來隔離,以保證插件進程崩潰不會對瀏覽器和頁面造成影響。每一種類型的插件對應一個進程,僅當使用該插件時才創建。

所以我們開啟一個頁面,至少會啟動4個進程。

3. HTTP 請求流程

HTTP是一種允許瀏覽器向服務器獲取資源的協議,是Web的基礎。通常由瀏覽器發起請求,用來獲取不同類型的文件,例如HTMLCSSJavaScript圖片視頻等。此外,HTTP也是瀏覽器使用最廣的協議。規定瞭客戶端請求,和服務器端響應數據格式的協議。

接下來簡單介紹一下 瀏覽器發送HTTP 請求的大致流程:

3.1 瀏覽器發送 HTTP 請求的流程

  1. 構造請求

首先,瀏覽器構造請求行,構建好之後,瀏覽器準備發起網絡請求

  1. 查找緩存

在正在發起網絡請求之前,瀏覽器會現在瀏覽器緩存中查詢是否有請求的文件,其實瀏覽器緩存是一種本地保存的資源副本,以供下次請求時直接使用的技術。

當瀏覽器發現請求的資源已經在瀏覽器緩存中存有副本,它會攔截請求,返回該資源的副本,並直接結束請求。而不會再去源服務器中重新下載。這樣可以緩解服務的壓力,提升性能。如果緩存查找失敗,則進入網絡請求。

  1. 準備IP地址和端口

HTTP和TCP的關系,因為瀏覽器使用HTTP協議作為應用層協議**,用來封裝請求的文本信息**;並使用TCP/IP作傳輸層協議將它發到網絡上,所以在HTTP工作開始之前,瀏覽器需要TCP與服務器建立連接。也就是說HTTP的內容是通過TCP的傳輸數據階段來實現的。

數據包是通過IP地址傳輸給接收方的。由於IP地址是數字標識的,難以記憶,使用一個域名例如www.baidu.com就容易記憶瞭,所以基於這個需求又出現瞭一個服務,負責把域名和IP地址做–映射關系。這套域名映射為IP的系統叫做”域名系統”,簡稱DNS

第一步瀏覽器會請求 DNS 返回域名對應的 IP。當然瀏覽器還提供瞭 DNS 數據緩存服務,如果某個域名已經解析過瞭,那麼瀏覽器會緩存解析的結果,以供下次查詢時直接使用,這樣也會減少一次網絡請求。

  1. 等待TCP隊列

IP地址和端口已經準備好瞭,是不是可以馬上建立TCP連接。

不行,因為Chrome有個機制,同一個域名同時最多隻能建立6個TCP連接。如果請求書少於6個,直接進入下一步,建立TCP連接。

  1. 建立TCP連接

排隊等待結束後,建立TCP連接

  1. 發送HTTP請求

3.2 服務端處理 HTTP 請求的流程

歷經千辛萬苦,HTTP 的請求信息終於被送達瞭服務器。接下來,服務器會根據瀏覽器的請求信息來準備相應的內容:

  1. 返回請求

  2. 斷開連接

通常情況下,一旦服務器向客戶端返回瞭請求數據,它就要關閉TCP連接。不過如果在瀏覽器或服務器在其頭部信息加入Connection:Keep-Alive那麼TCP連接在發送後將仍然保持打開狀態,這樣瀏覽器可以繼續通過同一個TCP連接發送請求。

保持TCP連接可以省去下次請求時需要建立連接的時間,提升資源加載速度。比如一個Web頁面中內嵌圖片來自於同一個web站點,如果初始化長連接,就不需要重復建立新的TCP連接。

3.3 為什麼很多站點第二次打開會很快

因為第一次加載頁面的過程中,緩存瞭一些耗時的數據。

那麼,哪些數據會被緩存呢?DNS緩存頁面資源緩存這兩塊數據是會被瀏覽器緩存的。

通過上圖第一次請求可以看出,當服務器返回HTTP響應頭給瀏覽器時,瀏覽器通過響應頭的Cache-Control字段來設置是否緩存該資源。

Cache-Control:Max-age=2000 //緩存過期時間是2000

這也就意味著,在該緩存資源還沒有過期的情況下,如果再次發送請求該資源,會之間返回緩存中的資源給瀏覽器。

但如果緩存過期瞭,瀏覽器則會繼續發送網絡請求,並且在HTTP請求頭中帶上:

If-None-Match:"4f80f-13c-3a1xb12a"

簡要來說,很多網站第二次訪問能夠秒開,是因為這些網站把很多資源都緩存在瞭本地,瀏覽器緩存直接使用本地副本來回應請求,而不會產生真實的網絡請求,從而節省瞭時間。同時,DNS 數據也被瀏覽器緩存瞭,這又省去瞭 DNS 查詢環節。

4. 輸入url地址到瀏覽器顯示頁面發生瞭什麼

接下來我們從進程角度討論一下:從瀏覽器裡,輸入URL地址,到頁面顯示,這中間發生瞭什麼?

從上圖可以看到,整個過程需要各個進程之間的配合,我們結合上圖我們從進程的角度,描述一下

1、瀏覽器進程接收到用戶輸入的URL請求,瀏覽器進程便將URL轉發給網絡進程。

2、網絡進程中發起真正的URL請求。

3、網絡進程接收到響應頭數據,便解析響應頭數據,並將數據轉發給瀏覽器進程

4、瀏覽器進程接收到網絡進程的響應頭數據之後,發送”提交文檔“消息到渲染進程

5、渲染進程接收到”提交文檔”的消息之後,便開始準備接收HTML數據,接收數據的方式是直接和網絡進程建立數據管道。

6、等文檔數據傳輸完成之後,渲染進程會返回“確認提交”的消息給瀏覽器進程

7、瀏覽器進程接收到渲染進程“確認提交”的消息之後,便開始移除之前舊的文檔,然後更新瀏覽器進程中的頁面狀態。

所謂提交文檔,就是瀏覽器主進程將網絡進程接收到的HTML數據提交給渲染進程

5. 瀏覽器渲染流程

接下來我們從一個簡單的html頁面來談瀏覽器的渲染流程:

5.1 構建 DOM 樹

DOM解析的特點,是不會被阻塞的。因為瀏覽器無法直接理解和使用HTML,所以需要將HTML轉化為瀏覽器能夠理解的結構—DOM樹。樹結構很像我們現實生活中的”樹”,其中的每一個點我們稱為**節點,**相連的節點稱為父子節點。在瀏覽器渲染中,我們使用的就是樹結構。

DOM樹描述瞭文檔的內容。元素是第一個標簽也是文檔樹的根節點。樹反映瞭不同標記之間的關系和層次結構。嵌套在其他標記中的標記是子節點。DOM節點的數量越多,構建DOM樹所需的時間就越長。

HTML內容轉換為瀏覽器DOM樹結構的過程:字節 → 字符 → 令牌 → 節點 → 對象模型。

  • 轉換: 瀏覽器從磁盤或網絡讀取 HTML 的原始字節,並根據文件的指定編碼(例如 UTF-8)將它們轉換成各個字符。
  • 令牌化: 瀏覽器將字符串轉換成 W3C HTML5 標準規定的各種令牌,例如,“”、“”,以及其他尖括號內的字符串。每個令牌都具有特殊含義和一組規則。
  • 詞法分析: 發出的令牌轉換成定義其屬性和規則的“對象”。
  • DOM 構建: 最後,由於 HTML 標記定義不同標記之間的關系(一些標記包含在其他標記內),創建的對象鏈接在一個樹數據結構內,此結構也會捕獲原始標記中定義的父項-子項關系: HTML 對象是 body 對象的父項,body 是 paragraph 對象的父項,依此類推。

當解析器發現非阻塞資源,例如一張圖片,瀏覽器會請求這些資源並且繼續解析。當遇到一個CSS文件時,解析也可以繼續進行,但是對於標簽(特別是沒有 async 或者 defer 屬性)會阻塞渲染並停止HTML的解析。

瀏覽器構建DOM樹時,這個過程占用瞭主線程。當這種情況發生時,預加載掃描儀將解析可用的內容並請求高優先級資源,如CSS、JavaScript和web字體。多虧瞭預加載掃描器,我們不必等到解析器找到對外部資源的引用來請求它。它將在後臺檢索資源,以便在主HTML解析器到達請求的資源時,它們可能已經在運行,或者已經被下載。預加載掃描儀提供的優化減少瞭阻塞。

5.2 樣式計算

先有內容,我們才能對內容就行修飾。

樣式計算的目的是為瞭計算出DOM節點中每一個元素的具體樣式,這個階段大體分三步。

5.2.1 把CSS轉換為瀏覽器內容理解的結構

CSS來源有:

  • 外部樣式表:通過link引用的CSS文件
  • 內部樣式表:style標簽內的CSS
  • 內聯樣式:元素的style屬性內嵌的CSS

和HTML文件一樣,瀏覽器也是無法直接理解這些純文本的CSS樣式,所以當渲染引擎接收到CSS文本的時,會執行一個轉換操作,將css文本轉換為瀏覽器可以理解的結構—styleSheets。

渲染引擎會把獲取到的 CSS 文本全部轉換為 styleSheets 結構中的數據,並且該結構同時具備瞭查詢和修改功能,這會為後面使用JS的樣式操作提供基礎。

5.2.2 轉換樣式表中的屬性值,使其標準化

我們已經將CSS轉換為瀏覽器能理解的結構瞭,那麼接下來就要對其進行屬性值的標準化操作。

那麼什麼是屬性值的標註啊呢?

body { font-size: 2em }p {color:blue;}span {display: none}div {font-weight: bold}div p {color:green;}div {color:red; }

可以看到上面的 CSS 文本中有很多屬性值,如 2embluebold,這些類型數值不容易被渲染引擎理解,所以需要將所有值轉換為渲染引擎容易理解的、標準化的計算值,這個過程就是屬性值標準化。

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

這裡就涉及到CSS繼承規則層疊規則瞭。

首先是CSS的繼承,css繼承是每個DOM節點都包含父節點的樣式。結合以下例子,看下面這張表示如何應用到DOM節點上的。

body { font-size: 20px }

繼承規則就是一般文本和字體相關樣式都是可以繼承的。層疊規則,嵌套的越深權重就越高。

總之,樣式計算階段的目的是為瞭計算出 DOM 節點中每個元素的具體樣式,在計算過程中需要遵守 CSS 的繼承和層疊兩個規則。這個階段最終輸出的內容是每個 DOM 節點的樣式,並被保存在 ComputedStyle 的結構內。

如果你想瞭解每個 DOM 元素最終的計算樣式,可以打開 Chrome 的“開發者工具”,選擇第一個“element”標簽,然後再選擇“Computed”子標簽

5.3 佈局階段

現在,我們有DOM樹和DOM樹中元素的樣式,但是還足以顯示頁面,因為我們還不知道DOM元素的幾何位置,那麼接下來就需要計算出DOM樹中可見元素的幾何位置,我們把這個計算過程叫做佈局

Chrome在佈局階段需要完成兩個任務:創建佈局樹和佈局計算

5.3.1 創建佈局樹

DOM樹有些元素不會在頁面上顯示,被用戶看到,如head標簽和使用瞭display:none的元素。所以在顯示之前,我麼還要額外地構建一棵隻包含瞭可見元素的佈局樹

從上圖可以看出,DOM樹中所有不可見的節點都沒有有包含到佈局樹中。

5.3.2 佈局計算

我們已經有瞭一棵完整的佈局樹,那麼接下來就要根據DOM節點對應的CSS樹中的樣式,計算佈局樹節點的坐標位置。即計算元素在視口上確切的位置和大小。

5.4 分層 (圖層樹)

有瞭佈局樹之後,每個元素的具體位置信息都計算出來瞭,那麼接下來是不是就要開始著手繪制頁面瞭?不是。因為頁面中有很多復雜的效果,如一些復雜的3D轉換頁面滾動,或者使用z-index,為瞭更方便的實現這些效果,渲染引擎還需要為特定的節點生成專門的圖層,並生成一棵對應的圖層樹(LayerTree)。這和PS的圖層類似,正是這些圖層疊加在一起才最終構成瞭頁面圖像。

想要直觀的理解什麼是圖層,可以打開Chrome的”開發工具”,選擇Layers標簽,就可以查看可視化頁面的分層情況。

佈局樹和圖層樹的關系

通常情況下,並不是佈局樹中的每一個節點都包含一個圖層,如果一個節點沒有對應的圖層,那麼這個節點就從屬於父節點的圖層。那麼什麼情況滿足,渲染引擎才會為特定的節點創建新的圖層呢?滿足一下兩個條件中的任意一個,元素就可以被單獨提升為一個圖層。

  1. 擁有層疊上下文屬性的元素會被提升為單獨的一層

頁面是一個二維平面,但層疊上下文能夠上HTML元素擁有三維概念,這些HTML元素按自身屬性的優先級分佈在垂直於這個二維平面的Z軸上,以下情況會作為單獨的圖層。

  • position:fixed
  • css 3d 例如:transform:rotateX(30deg)
  • video
  • canvas
  • CSS3動畫的節點
  • will-change
  1. 需要剪裁的地方也會被創建為圖層

那麼什麼是剪裁,結合以下代碼

<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <meta http-equiv="X-UA-Compatible" content="IE=edge">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>Document</title>    <style>        div {              width: 200px;              height: 200px;              overflow:auto;              background: gray;          }     </style></head><body>  <div >      <p>所以元素有瞭層疊上下文的屬性或者需要被剪裁,那麼就會被提升成為單獨一層,你可以參看下圖:</p>      <p>從上圖我們可以看到,document層上有A和B層,而B層之上又有兩個圖層。這些圖層組織在一起也是一顆樹狀結構。</p>      <p>圖層樹是基於佈局樹來創建的,為瞭找出哪些元素需要在哪些層中,渲染引擎會遍歷佈局樹來創建層樹(Update LayerTree)。</p>   </div></body></html>

這裡我麼把div的大小限定為200 * 200像素,而div裡面的文字內容比較多,文字所顯示的區域肯定會超過200 * 200的面積,這時候就產生瞭剪裁,渲染引擎會把裁剪文字內容的一部分用於顯示在div區域,下面是運行時的執行結果:

出現這種裁剪情況時,渲染引擎會為文字單獨為文字創建一層,如出現滾動條,滾動條也會被提升為單獨的層。

5.5 圖層的繪制

在完成圖層樹的構建之後,渲染引擎會對圖層樹中的每個圖層進行繪制,那麼接下來我們看看渲染引擎是如何實現圖層的繪制?

如果給你一張紙,讓你先把背景塗成暗色,然後再中間中間位置花一個紅色的圓,最後在圓上畫一個綠色三角,你會怎麼操作,通常你會按順序操作。

渲染引擎實現圖層的繪制與之類似,會把一個圖層的繪制拆分為很多小的繪制指令,然後再把這些指令按照順序組成一個待繪制列表,如下圖所示:

從圖中可以看出,繪制列表中的指令其實非常簡單,就是讓其執行一個簡單的繪制操作,比如說繪制粉色矩形或者黑色的線等。而繪制一個元素通常需要好幾條繪制指令,因為每個元素的背景、前景、邊框都需要單獨的指令去繪制。所以在圖層繪制階段,輸出的內容就是這些待繪制列表

5.6 柵格化操作

繪制列表指令用來記錄繪制順序和繪制指令的列表,而實際上繪制操作是由渲染引擎中的合成線程來完成。結合下圖看渲染主線程和合成線程之間的關系:

如上圖所示,當圖層的繪制列表準備好之後,主線程會把該繪制列表提交給合成線程,那麼合成線程是如何工作的?

首先我們談一個概念,視口。什麼是視口?

通常一個頁面可能很大,用戶隻能看到其中的一部分,我們把用戶可以看到的這個區域叫視口(viewport)。

比如說,一個圖層很大,頁面需要滾動底部,才能全部顯示。但是通過視口,用戶隻能看到頁面很小的一部分,所以在此種情況下,要一次性繪制完圖層所有的內容,會產生很大的開銷,且沒有必要。

基於這個原因,合成線程會將圖層劃分為圖塊,這些圖塊的大小通常是256 * 256或512 * 512。然後合成線程會按照視口附近的圖塊來優先生成位圖,實際生成位圖的操作就是有柵格化來執行的。所謂柵格化,**是指將圖塊轉化為位圖(所謂位圖就是能夠看的到的圖層區域)。而圖塊是柵格化執行的最小單位。**渲染進程維護瞭一個柵格化的線程池,所有的圖塊柵格化都是在線程池內執行,運行方式如下圖所示:

通常,柵格化過程都會使用GPU來加速生成,使用GPU生成位圖過程叫快速柵格化,或者GPU柵格化,生成的位圖被保存在GPU內存中。GPU操作是運行在GPU進程中的,那麼柵格化,還涉及到瞭跨進程操作。

從圖中可以看出,渲染進程把生成圖塊的指令發送給 GPU,然後在 GPU 中執行生成圖塊的位圖,並保存在 GPU 的內存中。

5.7 合成和顯示

一旦所有圖塊被柵格化,合成線程就會生成一個繪制圖塊的命令—“DrawQuad”,然後將該命令提交給瀏覽器進程。

瀏覽器進程裡有一個叫viz的組件,用來接收合成線程發過來的DrawQuad命令,然後根據DrawQuad命令,將其頁面內容繪制到內存中,最後顯示在屏幕上。

到此,經過一系列的階段,編寫好的HTMLCSSJavaScript等文件,經過瀏覽器就會顯示為頁面。

5.8 總結

我們已經完整分析瞭整個渲染流程,從HTML到DOM,樣式計算,佈局,圖層,繪制,柵格化,合成和顯示。

一個完整的渲染流程大致可總結如下:

  1. 渲染進程將HTML內容轉換為瀏覽器能夠讀懂的DOM樹結構。
  2. 渲染引擎將CSS樣式表轉化為瀏覽器能夠理解的CSS樹,計算出DOM節點的樣式。
  3. DOM樹 + CSS樹創建佈局樹,並計算元素的佈局信息。
  4. 對佈局樹進行分層,並生成圖層樹
  5. 對每個圖層生成繪制列表,並將其提交給合成線程。
  6. 對每個圖層進行單獨的繪制
  7. 合並圖層。

6. 相關概念

有瞭渲染流水線的基礎,我們來談談和渲染流水線關系的三個概念—重排重繪合成。理解這個三個概念對於後續Web的性能優化會有很大的幫助。

6.1 更新元素的幾何屬性(重排)

從上圖可以看出,如果你通過JS或CSS修改元素的幾何位置屬性,如widthheight等,那麼會觸發瀏覽器的重新佈局,解析之後的一系列子階段,這個過程就叫**重排也稱回流。重排需要更新完整的渲染流水線,所以開銷也最大的。

6.2 更新元素的繪制屬性(重繪)

比如通過JS更改某些元素的背景顏色,渲染流水的調整參見下圖:

修改元素的背景色,佈局階段不會執行,因為沒有引起幾何位置的變換,所以直接進入繪制,然後執行之後的一系列子階段,這個過程就叫重繪。相較重排操作,重繪省去瞭佈局和分層階段,所以執行效率會比重排效率高。

6.3 直接合成階段

那如果你更改一個既不要佈局也不要繪制的屬性,渲染引擎將跳過佈局和繪制,隻執行後續的合成操作,我們把這個過程叫做合成。

在上圖,我們使用CSStransform來實現動畫效果,可以避開重排和重繪階段,直接在非主線程上執行合成動畫操作。這樣的效率最高,因為是在非主線程上合成的,並沒有占用主線程的資源。

7. 優化方案

如果我們要提升性能,需要做的就是減少瀏覽器的重繪和回流

  • CSS
  1. 避免使用table佈局。
  2. 盡可能在DOM樹的最末端改變class。
  3. 避免設置多層內聯樣式。
  4. 將動畫效果應用到position屬性為absolute或fixed的元素上。
  5. 避免使用CSS表達式(例如:calc())。
  • JS
  1. 避免頻繁操作樣式,最好一次性重寫style屬性,或者將樣式列表定義為class並一次性更改class屬性。
  2. 避免頻繁操作DOM,創建一個documentFragment,在它上面應用所有DOM操作,最後再把它添加到文檔中。
  3. 也可以先為元素設置display: none,操作結束後再把它顯示出來。因為在display屬性為none的元素上進行的DOM操作不會引發回流和重繪。
  4. 避免頻繁讀取會引發回流/重繪的屬性,如果確實需要多次使用,就用一個變量緩存起來。
  5. 對具有復雜動畫的元素使用絕對定位,使它脫離文檔流,否則會引起父元素及後續元素頻繁回流。
本文來自網絡,不代表程式碼花園立場,如有侵權,請聯系管理員。https://www.codegarden.cn/article/5481/
返回顶部