你不知道的瀏覽器渲染原理

前言

瀏覽器的內核是指支持瀏覽器運行的最核心的程序,分爲兩個部分的,一是渲染引擎,另外一個是 JS 引擎。渲染引擎在不一樣的瀏覽器中也不是都相同的。目前市面上常見的瀏覽器內核能夠分爲這四種:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。這裏面你們最耳熟能詳的可能就是 Webkit 內核了,Webkit 內核是當下瀏覽器世界真正的霸主。html

本文咱們就以 Webkit 爲例,對現代瀏覽器的渲染過程進行一個深度的剖析。前端

頁面加載過程

在介紹瀏覽器渲染過程以前,咱們簡明扼要介紹下頁面的加載過程,有助於更好理解後續渲染過程。瀏覽器

要點以下:性能優化

  • 瀏覽器根據 DNS 服務器獲得域名的 IP 地址;服務器

  • 向這個 IP 的機器發送 HTTP 請求;網絡

  • 服務器收到、處理並返回 HTTP 請求;架構

  • 瀏覽器獲得返回內容。異步

例如在瀏覽器輸入https://juejin.im/timeline,而後通過 DNS 解析,juejin.im對應的 IP 是36.248.217.149(不一樣時間、地點對應的 IP 可能會不一樣)。而後瀏覽器向該 IP 發送 HTTP 請求。async

服務端接收到 HTTP 請求,而後通過計算(向不一樣的用戶推送不一樣的內容),返回 HTTP 請求,返回的內容以下:ide

image

其實就是一堆 HMTL 格式的字符串,由於只有 HTML 格式瀏覽器才能正確解析,這是 W3C 標準的要求。接下來就是瀏覽器的渲染過程。

瀏覽器渲染過程

瀏覽器渲染過程大致分爲以下三部分:

1)瀏覽器會解析三個東西:

一是 HTML/SVG/XHTML,HTML 字符串描述了一個頁面的結構,瀏覽器會把 HTML 結構字符串解析轉換 DOM 樹形結構。

二是 CSS,解析 CSS 會產生 CSS 規則樹,它和 DOM 結構比較像。

三是 Javascript 腳本,等到 Javascript 腳本文件加載後, 經過 DOM API 和 CSSOM API 來操做 DOM Tree 和 CSS Rule Tree。

2)解析完成後,瀏覽器引擎會經過 DOM Tree 和 CSS Rule Tree 來構造 Rendering Tree。

  • Rendering Tree 渲染樹並不等同於 DOM 樹,渲染樹只會包括須要顯示的節點和這些節點的樣式信息。

  • CSS 的 Rule Tree 主要是爲了完成匹配並把 CSS Rule 附加上 Rendering Tree 上的每一個 Element(也就是每一個 Frame)。

  • 而後,計算每一個 Frame 的位置,這又叫 layout 和 reflow 過程。

3)最後經過調用操做系統 Native GUI 的 API 繪製。

接下來咱們針對這其中所經歷的重要步驟詳細闡述

構建 DOM

瀏覽器會遵照一套步驟將 HTML 文件轉換爲 DOM 樹。宏觀上,能夠分爲幾個步驟:

構建 DOM 的具體步驟

瀏覽器從磁盤或網絡讀取 HTML 的原始字節,並根據文件的指定編碼(例如 UTF-8)將它們轉換成字符串。

在網絡中傳輸的內容其實都是 0 和 1 這些字節數據。當瀏覽器接收到這些字節數據之後,它會將這些字節數據轉換爲字符串,也就是咱們寫的代碼。

將字符串轉換成 Token,例如:<html><body>等。Token 中會標識出當前 Token 是「開始標籤」或是「結束標籤」亦或是「文本」等信息

這時候你必定會有疑問,節點與節點之間的關係如何維護?

事實上,這就是 Token 要標識「起始標籤」和「結束標籤」等標識的做用。例如「title」Token 的起始標籤和結束標籤之間的節點確定是屬於「head」的子節點。

上圖給出了節點之間的關係,例如:「Hello」Token 位於「title」開始標籤與「title」結束標籤之間,代表「Hello」Token 是「title」Token 的子節點。同理「title」Token 是「head」Token 的子節點。

  • 生成節點對象並構建 DOM

事實上,構建 DOM 的過程當中,不是等全部 Token 都轉換完成後再去生成節點對象,而是一邊生成 Token 一邊消耗 Token 來生成節點對象。換句話說,每一個 Token 被生成後,會馬上消耗這個 Token 建立出節點對象。注意:帶有結束標籤標識的 Token 不會建立節點對象。

接下來咱們舉個例子,假設有段 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>

上面這段 HTML 會解析成這樣:

構建 CSSOM

DOM 會捕獲頁面的內容,但瀏覽器還須要知道頁面如何展現,因此須要構建 CSSOM。

構建 CSSOM 的過程與構建 DOM 的過程很是類似,當瀏覽器接收到一段 CSS,瀏覽器首先要作的是識別出 Token,而後構建節點並生成 CSSOM。

在這一過程當中,瀏覽器會肯定下每個節點的樣式究竟是什麼,而且這一過程實際上是很消耗資源的。由於樣式你能夠自行設置給某個節點,也能夠經過繼承得到。在這一過程當中,瀏覽器得遞歸 CSSOM 樹,而後肯定具體的元素究竟是什麼樣式。

注意:CSS 匹配 HTML 元素是一個至關複雜和有性能問題的事情。因此,DOM 樹要小,CSS 儘可能用 id 和 class,千萬不要過渡層疊下去

構建渲染樹

當咱們生成 DOM 樹和 CSSOM 樹之後,就須要將這兩棵樹組合爲渲染樹。

在這一過程當中,不是簡單的將二者合併就好了。渲染樹只會包括須要顯示的節點和這些節點的樣式信息,若是某個節點是 display: none 的,那麼就不會在渲染樹中顯示。

咱們或許有個疑惑:瀏覽器若是渲染過程當中遇到 JS 文件怎麼處理

渲染過程當中,若是遇到<script>就中止渲染,執行 JS 代碼。由於瀏覽器有 GUI 渲染線程與 JS 引擎線程,爲了防止渲染出現不可預期的結果,這兩個線程是互斥的關係。JavaScript 的加載、解析與執行會阻塞 DOM 的構建,也就是說,在構建 DOM 時,HTML 解析器若遇到了 JavaScript,那麼它會暫停構建 DOM,將控制權移交給 JavaScript 引擎,等 JavaScript 引擎運行完畢,瀏覽器再從中斷的地方恢復 DOM 構建。

也就是說,若是你想首屏渲染的越快,就越不該該在首屏就加載 JS 文件,這也是都建議將 script 標籤放在 body 標籤底部的緣由。固然在當下,並非說 script 標籤必須放在底部,由於你能夠給 script 標籤添加 defer 或者 async 屬性(下文會介紹這二者的區別)。

JS 文件不僅是阻塞 DOM 的構建,它會致使 CSSOM 也阻塞 DOM 的構建

本來 DOM 和 CSSOM 的構建是互不影響,井水不犯河水,可是一旦引入了 JavaScript,CSSOM 也開始阻塞 DOM 的構建,只有 CSSOM 構建完畢後,DOM 再恢復 DOM 構建。

這是什麼狀況?

這是由於 JavaScript 不僅是能夠改 DOM,它還能夠更改樣式,也就是它能夠更改 CSSOM。由於不完整的 CSSOM 是沒法使用的,若是 JavaScript 想訪問 CSSOM 並更改它,那麼在執行 JavaScript 時,必需要能拿到完整的 CSSOM。因此就致使了一個現象,若是瀏覽器還沒有完成 CSSOM 的下載和構建,而咱們卻想在此時運行腳本,那麼瀏覽器將延遲腳本執行和 DOM 構建,直至其完成 CSSOM 的下載和構建。也就是說,在這種狀況下,瀏覽器會先下載和構建 CSSOM,而後再執行 JavaScript,最後在繼續構建 DOM

爲了幫助你們讓學習變得輕鬆、高效,給你們免費分享一大批資料,幫助你們在成爲全棧工程師,乃至架構師的路上披荊斬棘。在這裏給你們推薦一個前端全棧學習交流圈:866109386

佈局與繪製

當瀏覽器生成渲染樹之後,就會根據渲染樹來進行佈局(也能夠叫作迴流)。這一階段瀏覽器要作的事情是要弄清楚各個節點在頁面中的確切位置和大小。一般這一行爲也被稱爲「自動重排」。

佈局流程的輸出是一個「盒模型」,它會精確地捕獲每一個元素在視口內的確切位置和尺寸,全部相對測量值都將轉換爲屏幕上的絕對像素。

佈局完成後,瀏覽器會當即發出「Paint Setup」和「Paint」事件,將渲染樹轉換成屏幕上的像素。

以上咱們詳細介紹了瀏覽器工做流程中的重要步驟,接下來咱們討論幾個相關的問題:

幾點補充說明

1.async 和 defer 的做用是什麼?有什麼區別?

接下來咱們對比下 defer 和 async 屬性的區別:

async 和 defer

其中藍色線表明 JavaScript 加載;紅色線表明 JavaScript 執行;綠色線表明 HTML 解析。

1)狀況 1<script src="script.js"></script>

沒有 defer 或 async,瀏覽器會當即加載並執行指定的腳本,也就是說不等待後續載入的文檔元素,讀到就加載並執行。

2)狀況 2<script async src="script.js"></script> (異步下載)

async 屬性表示異步執行引入的 JavaScript,與 defer 的區別在於,若是已經加載好,就會開始執行——不管此刻是 HTML 解析階段仍是 DOMContentLoaded 觸發以後。須要注意的是,這種方式加載的 JavaScript 依然會阻塞 load 事件。換句話說,async-script 可能在 DOMContentLoaded 觸發以前或以後執行,但必定在 load 觸發以前執行。

3)狀況 3 <script defer src="script.js"></script>(延遲執行)

defer 屬性表示延遲執行引入的 JavaScript,即這段 JavaScript 加載時 HTML 並未中止解析,這兩個過程是並行的。整個 document 解析完畢且 defer-script 也加載完成以後(這兩件事情的順序無關),會執行全部由 defer-script 加載的 JavaScript 代碼,而後觸發 DOMContentLoaded 事件。

defer 與相比普通 script,有兩點區別:載入 JavaScript 文件時不阻塞 HTML 的解析,執行階段被放到 HTML 標籤解析完成以後。 在加載多個 JS 腳本的時候,async 是無順序的加載,而 defer 是有順序的加載。

2. 爲何操做 DOM 慢?

把 DOM 和 JavaScript 各自想象成一個島嶼,它們之間用收費橋樑鏈接。——《高性能 JavaScript》

JS 是很快的,在 JS 中修改 DOM 對象也是很快的。在 JS 的世界裏,一切是簡單的、迅速的。但 DOM 操做並不是 JS 一我的的獨舞,而是兩個模塊之間的協做。

由於 DOM 是屬於渲染引擎中的東西,而 JS 又是 JS 引擎中的東西。當咱們用 JS 去操做 DOM 時,本質上是 JS 引擎和渲染引擎之間進行了「跨界交流」。這個「跨界交流」的實現並不簡單,它依賴了橋接接口做爲「橋樑」(以下圖)。

過「橋」要收費——這個開銷自己就是不可忽略的。咱們每操做一次 DOM(無論是爲了修改仍是僅僅爲了訪問其值),都要過一次「橋」。過「橋」的次數一多,就會產生比較明顯的性能問題。所以「減小 DOM 操做」的建議,並不是空穴來風。

爲了幫助你們讓學習變得輕鬆、高效,給你們免費分享一大批資料,幫助你們在成爲全棧工程師,乃至架構師的路上披荊斬棘。在這裏給你們推薦一個前端全棧學習交流圈:866109386

3. 你真的瞭解迴流和重繪嗎?

渲染的流程基本上是這樣(以下圖黃色的四個步驟):

1. 計算 CSS 樣式

2. 構建 Render Tree

3.Layout – 定位座標和大小

4. 正式開畫

注意:上圖流程中有不少鏈接線,這表示了 Javascript 動態修改了 DOM 屬性或是 CSS 屬性會致使從新 Layout,但有些改變不會從新 Layout,就是上圖中那些指到天上的箭頭,好比修改後的 CSS rule 沒有被匹配到元素。

這裏重要要說兩個概念,一個是 Reflow,另外一個是 Repaint

重繪:當咱們對 DOM 的修改致使了樣式的變化、卻並未影響其幾何屬性(好比修改了顏色或背景色)時,瀏覽器不需從新計算元素的幾何屬性、直接爲該元素繪製新的樣式(跳過了上圖所示的迴流環節)。

迴流:當咱們對 DOM 的修改引起了 DOM 幾何尺寸的變化(好比修改元素的寬、高或隱藏元素等)時,瀏覽器須要從新計算元素的幾何屬性(其餘元素的幾何屬性和位置也會所以受到影響),而後再將計算的結果繪製出來,這個過程就是迴流(也叫重排)。

咱們知道,當網頁生成的時候,至少會渲染一次。在用戶訪問的過程當中,還會不斷從新渲染。從新渲染會重複迴流 + 重繪或者只有重繪。

迴流一定會發生重繪,重繪不必定會引起迴流。重繪和迴流會在咱們設置節點樣式時頻繁出現,同時也會很大程度上影響性能。迴流所需的成本比重繪高的多,改變父節點裏的子節點極可能會致使父節點的一系列迴流。

1)常見引發迴流屬性和方法

任何會改變元素幾何信息 (元素的位置和尺寸大小) 的操做,都會觸發迴流,

  • 添加或者刪除可見的 DOM 元素;

  • 元素尺寸改變——邊距、填充、邊框、寬度和高度;

  • 內容變化,好比用戶在 input 框中輸入文字;

  • 瀏覽器窗口尺寸改變——resize 事件發生時;

  • 計算 offsetWidth 和 offsetHeight 屬性;

  • 設置 style 屬性的值。

2)常見引發重繪屬性和方法

3)如何減小回流、重繪

  • 使用 transform 替代 top;

  • 使用 visibility 替換 display: none ,由於前者只會引發重繪,後者會引起迴流(改變了佈局);

  • 不要把節點的屬性值放在一個循環裏當成循環裏的變量。

for(let i = 0; i < 1000; i++) {
    // 獲取 offsetTop 會致使迴流,由於須要去獲取正確的值
    console.log(document.querySelector('.test').style.offsetTop)
}
  • 不要使用 table 佈局,可能很小的一個小改動會形成整個 table 的從新佈局;

  • 動畫實現的速度的選擇,動畫速度越快,迴流次數越多,也能夠選擇使用 requestAnimationFrame;

  • CSS 選擇符從右往左匹配查找,避免節點層級過多;

  • 將頻繁重繪或者回流的節點設置爲圖層,圖層可以阻止該節點的渲染行爲影響別的節點。好比對於 video 標籤來講,瀏覽器會自動將該節點變爲圖層。

性能優化策略

基於上面介紹的瀏覽器渲染原理,DOM 和 CSSOM 結構構建順序,初始化能夠對頁面渲染作些優化,提高頁面性能。

  • JS 優化: <script> 標籤加上 defer 屬性 和 async 屬性 用於在不阻塞頁面文檔解析的前提下,控制腳本的下載和執行。

  • defer 屬性: 用於開啓新的線程下載腳本文件,並使腳本在文檔解析完成後執行。

  • async 屬性: HTML5 新增屬性,用於異步下載腳本文件,下載完畢當即解釋執行代碼。

  • CSS 優化: <link> 標籤的 rel 屬性 中的屬性值設置爲 preload 可以讓你在你的 HTML 頁面中能夠指明哪些資源是在頁面加載完成後即刻須要的,最優的配置加載順序,提升渲染性能。

總結

綜上所述,咱們得出這樣的結論:

  • 瀏覽器工做流程:構建 DOM -> 構建 CSSOM -> 構建渲染樹 -> 佈局 -> 繪製。

  • CSSOM 會阻塞渲染,只有當 CSSOM 構建完畢後纔會進入下一個階段構建渲染樹。

  • 一般狀況下 DOM 和 CSSOM 是並行構建的,可是當瀏覽器遇到一個不帶 defer 或 async 屬性的 script 標籤時,DOM 構建將暫停,若是此時又恰巧瀏覽器還沒有完成 CSSOM 的下載和構建,因爲 JavaScript 能夠修改 CSSOM,因此須要等 CSSOM 構建完畢後再執行 JS,最後才從新 DOM 構建。

相關文章
相關標籤/搜索