瀏覽器頁面渲染機制

發送 & 接收信息

數據是以“數據包”的形式通過互聯網發送,而數據包以字節為單位。當你編寫一些 HTML、CSS 和 JS,并試圖在瀏覽器中打開 HTML 文件時,瀏覽器會從你的硬盤(或網絡)中讀取 HTML 的原始字節。明白了嗎?瀏覽器讀取的是原始數據字節,而不是你編寫的代碼的實際字符。

讓我們繼續。瀏覽器接收字節數據,但是,它用這些數據什么都做不了。數據的原始字節必須轉換為它所理解的形式,這是第一步。

從 HTML 的原始字節到 DOM

瀏覽器對象需要處理的是文檔對象模型(DOM)對象。那么,DOM 對象是從何而來的呢?這很簡單。首先,將原始數據字節轉換為字符。這一點,你可以通過你所編寫的代碼的字符看到。這種轉換是基于 HTML 文件的字符編碼完成的。至此,瀏覽器已經從原始數據字節轉換為文件中的實際字符。但這不是最終的結果。這些字符會被進一步解析為一些稱為“標記(token)”的東西

那么,這些標記是什么?文本文件中的一堆字符對瀏覽器引擎而言沒什么用處。如果沒有這個標記化過程,那么這一堆堆字符只會生成一系列毫無意義的文本,即 HTML 代碼——不會生成一個真正的網站。

當你保存一個擴展名為.html 的文件時,就向瀏覽器引擎發出了把文件解析為 HTML 文檔的信號。瀏覽器“解釋”這個文件的方式是首先解析它。在解析過程中,特別是在標記化過程中,瀏覽器會解析 HTML 文件中的每個開始和結束“標簽(tag)”。解析器可以識別尖括號中的每個字符串,如“< html>”、“< p>”,也可以推斷出適用于其中任何一個字符串的規則集。例如,表示錨標簽的標記與表示段落標簽的標記具有不同的屬性。

從概念上講,你可以將標記看作某種數據結構,它包含關于某個 HTML 標簽的信息。本質上,HTML 文件會被分解成稱為標記的小的解析單元。瀏覽器就是這樣開始識別你所編寫的內容的。

但標記還不是最終的結果。標記化完成后,接下來,標記將被轉換為節點。你可以將節點看作是具有特定屬性的不同對象。實際上,更好的解釋是,將節點看作是文檔對象樹中的獨立實體。但節點仍然不是最終結果。

現在,讓我們看一下最后一點。在創建好之后,這些節點將被鏈接到稱為 DOM 的樹數據結構中。DOM 建立起了父子關系、相鄰兄弟關系等。在這個 DOM 對象中,每個節點之間都建立了關系。現在,這是我們可以使用的東西了。

Bytes=> characters=>Tokens=>Node=>Dom

字節=>字符=>標記=>節點=>Dom樹

根據 HTML 文件的大小,DOM 的構建過程可能需要一些時間。無論文件多小,它都需要一些時間。

CSS 如何轉換?

DOM 已經創建。帶有一些 CSS 的典型 HTML 文件會包含下面這樣的樣式表鏈接:

<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" type="text/css" media="screen" href="main.css" />
</head>
<body>

</body>
</html>

當瀏覽器接收到原始數據字節并啟動 DOM 構建過程時,它還會發出請求來獲取鏈接的 main.css 樣式表。當瀏覽器開始解析 HTML 時,在找到 css 文件的鏈接標簽的同時,它會發出請求來獲取它。可能你已經猜到,瀏覽器還是接收 CSS 數據的原始字節,從互聯網或是本地磁盤。

但是,瀏覽器如何處理這些 CSS 數據的原始字節呢?

從 CSS 的原始字節到 CSSOM

當瀏覽器接收到 CSS 的原始字節時,會啟動一個和處理 HTML 原始字節類似的過程。就是說,原始數據字節被轉換成字符,然后標記,然后形成節點,最后形成樹結構。

什么是樹結構?大多數人都知道 DOM 這個詞。同樣,也有一種 CSS 樹結構,稱為 CSS 對象模型,簡稱為 CSSOM

你知道,瀏覽器不能使用 HTML 或 CSS 的原始字節。必須將其轉換成它能識別的形式,也就是這些樹形結構

CSS 有一個叫做級聯的東西。級聯是瀏覽器確定如何在元素上應用樣式的機制。

由于影響元素的樣式可能來自父元素,即通過繼承,或者已經在元素本身設置,所以 CSSOM 樹結構變得很重要。為什么?這是因為瀏覽器必須遞歸遍歷 CSS 樹結構并確定影響特定元素的樣式

一切順利。瀏覽器有了 DOM 和 CSSOM 對象。現在,我們能在屏幕上呈現一些東西了嗎?

渲染樹

我們現在得到的是兩個獨立的樹結構,它們似乎沒有共同的目標。

DOM 和 CSSOM 樹結構是兩個獨立的樹結構。DOM 中包含關于頁面 HTML 元素關系的所有信息,而 CSSOM 則包含關于元素樣式的信息。好了,瀏覽器現在把 DOM 和 CSSOM 樹組合成一棵渲染樹。

DOM + CSSOM = 渲染樹

渲染樹包含頁面上所有關于可見 DOM 內容的信息以及不同節點所需的所有 CSSOM 信息。注意,如果一個元素被 CSS 隱藏,例如使用 display; none,那么節點就不會包含在渲染樹中。隱藏元素會出現在 DOM 中,但不會出現在渲染樹中。這是因為渲染樹結合了來自 DOM 和 CSSOM 的信息,所以它知道不能把隱藏元素包含在樹中。

構建好渲染樹之后,瀏覽器將繼續下個步驟:布局!

布局

現在,我們有了屏幕上的內容和所有可見內容的樣式信息——但是我們并沒有實際在屏幕上渲染任何內容。首先,瀏覽器必須計算頁面上每個對象的確切大小和位置。這就好比是,把要在頁面上渲染的所有元素的內容和樣式信息傳遞給一個有才華的數學家。然后,這位數學家用瀏覽器的視窗計算出每個元素的確切位置和大小。

這個布局步驟考慮了從 DOM 和 CSSOM 接收到的內容和樣式,并執行了所有必要的布局計算。有時,你會聽到人們把這個“布局”階段稱為“回流(reflow)

藝術家出場

現在,每個元素的確切位置已經計算出來,剩下的就是將元素“繪制”到屏幕上。

考慮一下。我們已經得到了在屏幕上顯示元素所需的所有信息。我們只要把它展示給用戶。這就是這個階段的全部工作。有了元素內容(DOM)樣式(CSSOM)計算得出的元素的精確布局信息,瀏覽器現在就可以將節點逐個“繪制”到屏幕上了。元素現在終于呈現在屏幕上了!

渲染阻塞資源

當你聽到“渲染阻塞(render-blocking)”時,你會想到什么?我猜你想的是,“有東西阻止了屏幕上節點的實際繪制”。如果你這么說,那你說的完全正確!

優化網站的第一準則是讓最重要的 HTML 和 CSS 盡可能快地傳遞到客戶端。在成功繪制之前,必須構造 DOM 和 CSSOM,因此,HTML 和 CSS 都是渲染阻塞資源。關鍵是,你應該盡快將 HTML 和 CSS 提供給客戶端,以優化應用程序的首次渲染時間。

JavaScript 如何執行?

一個好的 Web 應用程序肯定會使用一些 JavaScript。這是一定的。JavaScript 的“問題”在于你可以使用 JavaScript 修改頁面的內容和樣式。通過這種方式,你可以從 DOM 樹中刪除元素和添加元素,還可以通過 JavaScript 修改元素的 CSSOM 屬性

<!DOCTYPE html>
<html>

<head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Medium Article Demo</title>
    <link rel="stylesheet" href="style.css">
</head>

<body>
    <p id="header">How Browser Rendering Works</p>
    <div><img src="https://i.imgur.com/jDq3k3r.jpg"></div>
</body>

</html>

這是一個非常簡單的文檔。樣式表 style.css 只有下面一個聲明語句:

body {
  background: #8cacea;
}

根據前面的解釋,瀏覽器從磁盤(或網絡)讀取 HTML 文件的原始字節并將其轉換為字符。字符被進一步解析為標記。當解析器遇到< link rel="stylesheet" href="style.css">時,就會請求獲取 CSS 文件 style.css。DOM 構造繼續進行,當 CSS 文件返回一些內容后,CSSOM 構造就開始了

引入 JavaScript 后,這個過程會發生什么變化?要記住,其中最重要的一件事情是,每當瀏覽器遇到腳本標簽時,DOM 構造就會暫停!整個 DOM 構建過程都將停止,直到腳本執行完成

這是因為 JavaScript 可以同時修改 DOM 和 CSSOM。由于瀏覽器不確定特定的 JavaScript 會做什么,所以它采取的預防措施是停止整個 DOM 構造。

<!DOCTYPE html>
<html>

<head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Medium Article Demo</title>
    <link rel="stylesheet" href="style.css">
</head>

<body>

    <p id="header">How Browser Rendering Works</p>
    <div><img src="https://i.imgur.com/jDq3k3r.jpg"></div>

    <script>
        let header = document.getElementById("header");

        console.log("header is: ", header);
    </script>
</body>

</html>

在腳本標簽中,我將訪問 id 為 header 的 DOM 節點,然后將其輸出到控制臺。可以正常運行。

但是,你是否注意到,這個腳本標簽位于 body 標簽的底部?讓我們把它放在 head 中,看看會發生什么:一旦我這樣做,header 就解析為 null。

為什么會這樣?很簡單。當 HTML 解析器正在構建 DOM 時,發現了一個腳本標簽。此時,body 標簽及其所有內容還沒有被解析。DOM 構造將停止,直到腳本執行完成:

當腳本試圖訪問一個 id 為 header 的 DOM 節點時,由于 DOM 還沒有完成對文檔的解析,所以它還不存在。這把我們帶到了另一個重要的問題。腳本的位置很重要。

這還不是全部。如果你將內聯腳本提取到外部本地文件 app.js 中,行為是一樣的。DOM 的構建仍然會停止:

<!DOCTYPE html>
<html>

<head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Medium Article Demo</title>
    <link rel="stylesheet" href="style.css">
    <script src="app.js"></script>
</head>

<body>

    <p id="header">How Browser Rendering Works</p>
    <div><img src="https://i.imgur.com/jDq3k3r.jpg"></div>
</body>

</html>

那么,如果 app.js 不是本地的而必須通過互聯網獲取呢?如果網速很慢,需要數千毫秒來獲取 app.js,DOM 的構建也會暫停幾千毫秒!!!這是一個很大的性能問題,而且還不止于此。JavaScript 還可以訪問 CSSOM 并對其進行修改。例如,這是有效的 JavaScript 語句:

document.getElementsByTagName("body")[0].style.backgroundColor = "red";

那么,當解析器遇到一個腳本標簽而 CSSOM 還沒有準備好時,會發生什么情況呢?答案很簡單。Javascript 執行將會停止,直到 CSSOM 就緒。因此,雖然 DOM 構造在遇到腳本標簽時會停止,但 CSSOM 不會發生這種情況。對于 CSSOM,JS 執行會等待。沒有 CSSOM,就沒有 JS 執行

async 屬性

在默認情況下,每個腳本都是一個解析器阻斷器!DOM 的構建總是會被打斷。不過,有一種方法可以改變這種默認行為。如果將 async 關鍵字添加到腳本標簽中,那么 DOM 構造就不會停止。DOM 構造將繼續,腳本將在下載完成并準備就緒后執行

<!DOCTYPE html>
<html>

<head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Medium Article Demo</title>
    <link rel="stylesheet" href="style.css">
    <script src="https://some-link-to-app.js" async></script>
</head>

<body>

    <p id="header">How Browser Rendering Works</p>
    <div><img src="https://i.imgur.com/jDq3k3r.jpg"></div>
</body>

</html>

關鍵渲染路徑

目前為止,我們討論了從接收 HTML、CSS 和 JS 字節到將它們轉換為屏幕上的像素之間的所有步驟。這整個過程稱為關鍵渲染路徑。優化網站性能就是優化關鍵渲染路徑。

一個經過良好優化的站點應該能夠漸進式渲染,而不是讓整個過程受阻。

這是 Web 應用程序慢或快的區別所在。周密的關鍵渲染路徑(CRP)優化策略使瀏覽器能夠通過確定優先加載的資源以及資源加載的順序來盡可能快地加載頁面。

常見引起回流屬性和方法

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

  • 添加或者刪除可見的DOM元素;
  • 元素尺寸改變——邊距、填充、邊框、寬度和高度
  • 內容變化,比如用戶在input框中輸入文字
  • 瀏覽器窗口尺寸改變——resize事件發生時
  • 計算 offsetWidth 和 offsetHeight 屬性
  • 設置 style 屬性的值

常見引起重繪屬性和方法

下面例子中,觸發了幾次回流和重繪?

var s = document.body.style;
s.padding = "2px"; // 回流+重繪
s.border = "1px solid red"; // 再一次 回流+重繪
s.color = "blue"; // 再一次重繪
s.backgroundColor = "#ccc"; // 再一次 重繪
s.fontSize = "14px"; // 再一次 回流+重繪
// 添加node,再一次 回流+重繪
document.body.appendChild(document.createTextNode('abc!'));

如何減少回流、重繪

  • 使用 transform 替代 top
  • 使用 visibility 替換 display: none ,因為前者只會引起重繪,后者會引發回流(改變了布局)
  • 不要把節點的屬性值放在一個循環里當成循環里的變量。
  • 不要使用 table 布局,可能很小的一個小改動會造成整個 table 的重新布局
  • 動畫實現的速度的選擇,動畫速度越快,回流次數越多,也可以選擇使用 requestAnimationFrame
  • CSS 選擇符從右往左匹配查找,避免節點層級過多
  • 將頻繁重繪或者回流的節點設置為圖層,圖層能夠阻止該節點的渲染行為影響別的節點。比如對于 video 標簽來說,瀏覽器會自動將該節點變為圖層。

async和defer的作用是什么?有什么區別?

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

(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是有順序的加載。

為什么操作 DOM 慢

因為 DOM 是屬于渲染引擎中的東西,而 JS 又是 JS 引擎中的東西。當我們通過 JS 操作 DOM 的時候,其實這個操作涉及到了兩個線程之間的通信,那么勢必會帶來一些性能上的損耗。操作 DOM 次數一多,也就等同于一直在進行線程之間的通信,并且操作 DOM 可能還會帶來重繪回流的情況,所以也就導致了性能上的問題。

渲染頁面時常見哪些不良現象?

由于瀏覽器的渲染機制不同,在渲染頁面時會出現兩種常見的不良現象----白屏問題和FOUS(無樣式內容閃爍)

FOUC:由于瀏覽器渲染機制(比如firefox),再CSS加載之前,先呈現了HTML,就會導致展示出無樣式內容,然后樣式突然呈現的現象;

白屏:有些瀏覽器渲染機制(比如chrome)要先構建DOM樹和CSSOM樹,構建完成后再進行渲染,如果CSS部分放在HTML尾部,由于CSS未加載完成,瀏覽器遲遲未渲染,從而導致白屏;也可能是把js文件放在頭部,腳本會阻塞后面內容的呈現,腳本會阻塞其后組件的下載,出現白屏問題。

總結

  1. 瀏覽器工作流程:構建DOM -> 構建CSSOM -> 構建渲染樹 -> 布局 -> 繪制。
  2. 當瀏覽器接收到原始數據字節并啟動 DOM 構建過程時,它還會發出請求來獲取鏈接的 main.css 樣式表,啟動CSSOM構建
  3. 構建DOM的過程中,不是等所有Token都轉換完成后再去生成節點對象,而是一邊生成Token一邊消耗Token來生成節點對象。換句話說,每個Token被生成后,會立刻消耗這個Token創建出節點對象
  4. DOM 和 CSSOM 樹結構是兩個獨立的樹結構。DOM 中包含關于頁面 HTML 元素關系的所有信息,而 CSSOM 則包含關于元素樣式的信息。
  5. 瀏覽器得遞歸 CSSOM 樹,然后確定具體的元素到底是什么樣式,注意:CSS匹配HTML元素是一個相當復雜和有性能問題的事情。所以,DOM樹要小,CSS盡量用id和class,千萬不要過渡層疊下去
  6. CSSOM會阻塞渲染,只有當CSSOM構建完畢后才會進入下一個階段構建渲染樹。
  7. 渲染樹包含頁面上所有關于可見 DOM 內容的信息以及不同節點所需的所有 CSSOM 信息,隱藏元素會出現在 DOM 中,但不會出現在渲染樹中。這是因為渲染樹結合了來自 DOM 和 CSSOM 的信息,所以它知道不能把隱藏元素包含在樹中。
  8. 構建好渲染樹之后,瀏覽器必須計算頁面上每個對象的確切大小和位置,這個布局步驟考慮了從 DOM 和 CSSOM 接收到的內容和樣式,并執行了所有必要的布局計算(回流或者自動重排)。
  9. 每當瀏覽器遇到腳本標簽時,DOM 構造就會暫停!整個 DOM 構建過程都將停止,但 CSSOM 不會發生這種情況,直到腳本執行完成,當解析器遇到一個腳本標簽而 CSSOM 還沒有準備好時,Javascript 執行將會停止,直到 CSSOM 就緒,對于 CSSOM,JS 執行會等待。
  10. 通常情況下DOM和CSSOM是并行構建的,但是當瀏覽器遇到一個script標簽時,DOM構建將暫停,直至腳本完成執行。但由于JavaScript可以修改CSSOM,所以需要等CSSOM構建完畢后再執行JS。
  11. JS文件不只是阻塞DOM的構建,它會導致CSSOM也阻塞DOM的構建。原本DOM和CSSOM的構建是互不影響,井水不犯河水,但是一旦引入了JavaScript,CSSOM也開始阻塞DOM的構建,只有CSSOM構建完畢后,DOM再恢復DOM構建。在這種情況下,瀏覽器會先下載和構建CSSOM,然后再執行JavaScript,最后在繼續構建DOM。
  12. 如果你想首屏渲染的越快,就越不應該在首屏就加載 JS 文件,建議將 script 標簽放在 body 標簽底部。
  13. 重繪:當render tree中的一些元素需要更新屬性,而這些屬性只是影響元素的外觀、風格,而不會影響布局的,比如background-color。
  14. 回流:當render tree中的一部分(或全部)因為元素的規模尺寸、布局、隱藏等改變而需要重新構建
  15. 回流必定會發生重繪,重繪不一定會引發回流。重繪和回流會在我們設置節點樣式時頻繁出現,同時也會很大程度上影響性能。回流所需的成本比重繪高的多,改變父節點里的子節點很可能會導致父節點的一系列回流。

參考文檔

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 大家都知道萬維網的應用層使用了HTTP協議,并且用瀏覽器作為入口訪問網絡上的資源。用戶在使用瀏覽器訪問一個網站時需...
    SylvanasSun閱讀 2,162評論 1 12
  • 瀏覽器指的是Chrome系瀏覽器【Firefox大同小異,IE未知】以下提到的“節點”、“標簽”和“元素”不做區分...
    Yieiy閱讀 4,125評論 4 26
  • 關鍵渲染路徑 瀏覽器從接收到頁面開始到頁面顯示,這整個過程中的所有步驟,稱為關鍵渲染路徑。用戶看到頁面實際上可以分...
    ZombieBrandg閱讀 510評論 0 0
  • 親愛的 楓葉又紅了 紅葉也瘋了 正午 陽光好暖 放下吧 就曬太陽 就目空一切 就無所事事 就一事無成 就這樣 呆在陽光里
    一片_楓葉閱讀 258評論 0 0
  • 有個小男孩,暑假被麻麻糖衣加炮彈的哄騙去了新東方上幼小銜接課程,幼小銜接這個名詞我是在五年前知道的,當時娃尚在襁褓...
    陳娟米奇閱讀 590評論 1 6