瀏覽器渲染原理
- 瀏覽器渲染原理
- 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進程架構圖
瀏覽器設置的時候是一個多進程模型,這樣能確保瀏覽的安全性和穩定性。如果一個頁面有問題,不影響其他頁面的運行。
- 瀏覽器進程。主要負責界面顯示、用戶交互、子進程管理、同時提供存儲等功能。
- 渲染進程。 核心任務是將
HTML
、CSS
和JavaScript
轉換為用戶可以與之交互的網頁,排版引擎Blink
和JavaScript
引擎V8都運行在該進程中,默認情況下,Chrome
為每一個Tab
標簽頁創建一個渲染進程。出於安全考慮,渲染進程都是運行在沙箱模式下的。 - **GPU進程。**GPU圖形處理器(英語:graphics processing unit,縮寫:GPU),負責3D CSS效果,網頁,Chrome ui的繪制。
- 網絡進程。主要負責頁面的網絡資源加載,之前是作為一個模塊運行在瀏覽器進程裡面的,直至最近才獨立處理,成為單獨一個進程。
- 插件進程。主要負責插件的運行,因為插件易崩潰,所以通過插件進程來隔離,以保證插件進程崩潰不會對瀏覽器和頁面造成影響。每一種類型的插件對應一個進程,僅當使用該插件時才創建。
所以我們開啟一個頁面,至少會啟動4個進程。
3. HTTP 請求流程
HTTP
是一種允許瀏覽器向服務器獲取資源的協議,是Web的基礎。通常由瀏覽器發起請求,用來獲取不同類型的文件,例如HTML
,CSS
,JavaScript
、圖片、視頻等。此外,HTTP
也是瀏覽器使用最廣的協議。規定瞭客戶端請求,和服務器端響應數據格式的協議。
接下來簡單介紹一下 瀏覽器發送HTTP
請求的大致流程:
3.1 瀏覽器發送 HTTP 請求的流程
- 構造請求
首先,瀏覽器構造請求行,構建好之後,瀏覽器準備發起網絡請求
- 查找緩存
在正在發起網絡請求之前,瀏覽器會現在瀏覽器緩存中查詢是否有請求的文件,其實瀏覽器緩存是一種本地保存的資源副本,以供下次請求時直接使用的技術。
當瀏覽器發現請求的資源已經在瀏覽器緩存中存有副本,它會攔截請求,返回該資源的副本,並直接結束請求。而不會再去源服務器中重新下載。這樣可以緩解服務的壓力,提升性能。如果緩存查找失敗,則進入網絡請求。
- 準備IP地址和端口
HTTP和TCP的關系,因為瀏覽器使用HTTP協議作為應用層協議**,用來封裝請求的文本信息**;並使用TCP/IP作傳輸層協議將它發到網絡上,所以在HTTP工作開始之前,瀏覽器需要TCP與服務器建立連接。也就是說HTTP的內容是通過TCP的傳輸數據階段來實現的。
數據包是通過IP地址傳輸給接收方的。由於IP地址是數字標識的,難以記憶,使用一個域名例如www.baidu.com就容易記憶瞭,所以基於這個需求又出現瞭一個服務,負責把域名和IP地址做–映射關系。這套域名映射為IP的系統叫做”域名系統”,簡稱DNS
。
第一步瀏覽器會請求 DNS 返回域名對應的 IP。當然瀏覽器還提供瞭 DNS 數據緩存服務,如果某個域名已經解析過瞭,那麼瀏覽器會緩存解析的結果,以供下次查詢時直接使用,這樣也會減少一次網絡請求。
- 等待TCP隊列
IP地址和端口已經準備好瞭,是不是可以馬上建立TCP連接。
不行,因為Chrome有個機制,同一個域名同時最多隻能建立6個TCP連接。如果請求書少於6個,直接進入下一步,建立TCP連接。
- 建立TCP連接
排隊等待結束後,建立TCP連接
- 發送HTTP請求
3.2 服務端處理 HTTP 請求的流程
歷經千辛萬苦,HTTP 的請求信息終於被送達瞭服務器。接下來,服務器會根據瀏覽器的請求信息來準備相應的內容:
返回請求
斷開連接
通常情況下,一旦服務器向客戶端返回瞭請求數據,它就要關閉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
文本中有很多屬性值,如 2em
、blue
、bold
,這些類型數值不容易被渲染引擎理解,所以需要將所有值轉換為渲染引擎容易理解的、標準化的計算值,這個過程就是屬性值標準化。
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
標簽,就可以查看可視化頁面的分層情況。
佈局樹和圖層樹的關系
通常情況下,並不是佈局樹中的每一個節點都包含一個圖層,如果一個節點沒有對應的圖層,那麼這個節點就從屬於父節點的圖層。那麼什麼情況滿足,渲染引擎才會為特定的節點創建新的圖層呢?滿足一下兩個條件中的任意一個,元素就可以被單獨提升為一個圖層。
- 擁有層疊上下文屬性的元素會被提升為單獨的一層
頁面是一個二維平面,但層疊上下文能夠上HTML
元素擁有三維概念,這些HTML
元素按自身屬性的優先級分佈在垂直於這個二維平面的Z軸上,以下情況會作為單獨的圖層。
position:fixed
css 3d
例如:transform:rotateX(30deg)
video
canvas
- 有
CSS3
動畫的節點 will-change
- 需要剪裁的地方也會被創建為圖層
那麼什麼是剪裁,結合以下代碼
<!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命令,將其頁面內容繪制到內存中,最後顯示在屏幕上。
到此,經過一系列的階段,編寫好的HTML
、CSS
、JavaScript
等文件,經過瀏覽器就會顯示為頁面。
5.8 總結
我們已經完整分析瞭整個渲染流程,從HTML到DOM,樣式計算,佈局,圖層,繪制,柵格化,合成和顯示。
一個完整的渲染流程大致可總結如下:
- 渲染進程將HTML內容轉換為瀏覽器能夠讀懂的DOM樹結構。
- 渲染引擎將CSS樣式表轉化為瀏覽器能夠理解的CSS樹,計算出DOM節點的樣式。
- DOM樹 + CSS樹創建佈局樹,並計算元素的佈局信息。
- 對佈局樹進行分層,並生成圖層樹。
- 對每個圖層生成繪制列表,並將其提交給合成線程。
- 對每個圖層進行單獨的繪制
- 合並圖層。
6. 相關概念
有瞭渲染流水線的基礎,我們來談談和渲染流水線關系的三個概念—重排,重繪和合成。理解這個三個概念對於後續Web的性能優化會有很大的幫助。
6.1 更新元素的幾何屬性(重排)
從上圖可以看出,如果你通過JS或CSS修改元素的幾何位置屬性,如width
,height
等,那麼會觸發瀏覽器的重新佈局,解析之後的一系列子階段,這個過程就叫**重排也稱回流。重排需要更新完整的渲染流水線,所以開銷也最大的。
6.2 更新元素的繪制屬性(重繪)
比如通過JS更改某些元素的背景顏色,渲染流水的調整參見下圖:
修改元素的背景色,佈局階段不會執行,因為沒有引起幾何位置的變換,所以直接進入繪制,然後執行之後的一系列子階段,這個過程就叫重繪。相較重排操作,重繪省去瞭佈局和分層階段,所以執行效率會比重排效率高。
6.3 直接合成階段
那如果你更改一個既不要佈局也不要繪制的屬性,渲染引擎將跳過佈局和繪制,隻執行後續的合成操作,我們把這個過程叫做合成。
在上圖,我們使用CSS
的transform
來實現動畫效果,可以避開重排和重繪階段,直接在非主線程上執行合成動畫操作。這樣的效率最高,因為是在非主線程上合成的,並沒有占用主線程的資源。
7. 優化方案
如果我們要提升性能,需要做的就是減少瀏覽器的重繪和回流
- CSS
- 避免使用table佈局。
- 盡可能在DOM樹的最末端改變class。
- 避免設置多層內聯樣式。
- 將動畫效果應用到position屬性為absolute或fixed的元素上。
- 避免使用CSS表達式(例如:calc())。
- JS
- 避免頻繁操作樣式,最好一次性重寫style屬性,或者將樣式列表定義為class並一次性更改class屬性。
- 避免頻繁操作DOM,創建一個documentFragment,在它上面應用所有DOM操作,最後再把它添加到文檔中。
- 也可以先為元素設置
display: none
,操作結束後再把它顯示出來。因為在display
屬性為none
的元素上進行的DOM操作不會引發回流和重繪。 - 避免頻繁讀取會引發回流/重繪的屬性,如果確實需要多次使用,就用一個變量緩存起來。
- 對具有復雜動畫的元素使用絕對定位,使它脫離文檔流,否則會引起父元素及後續元素頻繁回流。