瀏覽器的渲染:過程與原理

瀏覽器渲染頁面的過程

從耗時的角度,瀏覽器請求、加載、渲染一個頁面,時間花在下面五件事情上:

DNS 查詢

TCP 連接

HTTP 請求即響應(yīng)

服務(wù)器響應(yīng)

客戶端渲染

本文討論第五個部分,即瀏覽器對內(nèi)容的渲染,這一部分(渲染樹構(gòu)建、布局及繪制),又可以分為下面五個步驟

處理 HTML 標(biāo)記并構(gòu)建 DOM 樹。

處理 CSS 標(biāo)記并構(gòu)建 CSSOM 樹。

將 DOM 與 CSSOM 合并成一個渲染樹。

根據(jù)渲染樹來布局,以計算每個節(jié)點(diǎn)的幾何信息。

將各個節(jié)點(diǎn)繪制到屏幕上。

需要明白,這五個步驟并不一定一次性順序完成。如果 DOM 或 CSSOM 被修改,以上過程需要重復(fù)執(zhí)行,這樣才能計算出哪些像素需要在屏幕上進(jìn)行重新渲染。實(shí)際頁面中,CSS 與 JavaScript 往往會多次修改 DOM 和 CSSOM,下面就來看看它們的影響方式。

阻塞渲染:CSS 與 JavaScript

談?wù)撡Y源的阻塞時,我們要清楚,現(xiàn)代瀏覽器總是并行加載資源。例如,當(dāng) HTML 解析器(HTML Parser)被腳本阻塞時,解析器雖然會停止構(gòu)建 DOM,但仍會識別該腳本后面的資源,并進(jìn)行預(yù)加載。

同時,由于下面兩點(diǎn):


默認(rèn)情況下,CSS 被視為阻塞渲染的資源,這意味著瀏覽器將不會渲染任何已處理的內(nèi)容,直至 CSSOM 構(gòu)建完畢。


JavaScript 不僅可以讀取和修改 DOM 屬性,還可以讀取和修改 CSSOM 屬性。



存在阻塞的 CSS 資源時,瀏覽器會延遲 JavaScript 的執(zhí)行和 DOM 構(gòu)建。另外:


當(dāng)瀏覽器遇到一個 script 標(biāo)記時,DOM 構(gòu)建將暫停,直至腳本完成執(zhí)行。


JavaScript 可以查詢和修改 DOM 與 CSSOM。


CSSOM 構(gòu)建時,JavaScript 執(zhí)行將暫停,直至 CSSOM 就緒。



所以,script 標(biāo)簽的位置很重要。實(shí)際使用時,可以遵循下面兩個原則:


CSS 優(yōu)先:引入順序上,CSS 資源先于 JavaScript 資源。


JavaScript 應(yīng)盡量少影響 DOM 的構(gòu)建。



瀏覽器的發(fā)展日益加快(目前的 Chrome 官方穩(wěn)定版是 61),具體的渲染策略會不斷進(jìn)化,但了解這些原理后,就能想通它進(jìn)化的邏輯。下面來看看 CSS 與 JavaScript 具體會怎樣阻塞資源。

CSS

<style> p { color: red; }</style>

<link rel="stylesheet" href="index.css">

這樣的 link 標(biāo)簽(無論是否 inline)會被視為阻塞渲染的資源,瀏覽器會優(yōu)先處理這些 CSS 資源,直至 CSSOM 構(gòu)建完畢。

渲染樹(Render-Tree)的關(guān)鍵渲染路徑中,要求同時具有 DOM 和 CSSOM,之后才會構(gòu)建渲染樹。即,HTML 和 CSS 都是阻塞渲染的資源。HTML 顯然是必需的,因?yàn)榘ㄎ覀兿M@示的文本在內(nèi)的內(nèi)容,都在 DOM 中存放,那么可以從 CSS 上想辦法。

最容易想到的當(dāng)然是精簡 CSS 并盡快提供它。除此之外,還可以用媒體類型(media type)和媒體查詢(media query)來解除對渲染的阻塞。

<link href="index.css" rel="stylesheet">

<link href="print.css" rel="stylesheet"media="print">

<link href="other.css" rel="stylesheet" media="(min-width: 30em) and (orientation: landscape)">


第一個資源會加載并阻塞。
第二個資源設(shè)置了媒體類型,會加載但不會阻塞,print 聲明只在打印網(wǎng)頁時使用。
第三個資源提供了媒體查詢,會在符合條件時阻塞渲染。

JavaScript

JavaScript 的情況比 CSS 要更復(fù)雜一些。觀察下面的代碼:

<p>Do not go gentle into that good night,</p>

<script>console.log("inline")</script>

<p>Old age should burn and rave at close of day;</p>

<script src="app.js"></script>

<p>Rage, rage against the dying of the light.</p>


<p>Do not go gentle into that good night,</p>

<script src="app.js"></script>

<p>Old age should burn and rave at close of day;</p>

<script>console.log("inline")</script>

<p>Rage, rage against the dying of the light.</p>


這樣的 script 標(biāo)簽會阻塞 HTML 解析,無論是不是 inline-script。上面的 P 標(biāo)簽會從上到下解析,這個過程會被兩段 JavaScript 分別打算一次(加載、執(zhí)行)。

所以實(shí)際工程中,我們常常將資源放到文檔底部。

改變阻塞模式:defer 與 async

為什么要將 script 加載的 defer 與 async 方式放到后面呢?因?yàn)檫@兩種方式是的出現(xiàn),全是由于前面講的那些阻塞條件的存在。換句話說,defer 與 async 方式可以改變之前的那些阻塞情形。

首先,注意 async 與 defer 屬性對于 inline-script 都是無效的,所以下面這個示例中三個 script 標(biāo)簽的代碼會從上到下依次執(zhí)行。

<!-- 按照從上到下的順序輸出 1 2 3 -->

<script async>

? console.log("1");

</script>

<script defer>

? console.log("2");

</script>

<script>

? console.log("3");

</script>


故,下面兩節(jié)討論的內(nèi)容都是針對設(shè)置了 src 屬性的 script 標(biāo)簽。

defer

<script src="app1.js" defer></script>

<script src="app2.js" defer></script>

<script src="app3.js" defer></script>


defer 屬性表示延遲執(zhí)行引入的 JavaScript,即這段 JavaScript 加載時 HTML 并未停止解析,這兩個過程是并行的。整個 document 解析完畢且 defer-script 也加載完成之后(這兩件事情的順序無關(guān)),會執(zhí)行所有由 defer-script 加載的 JavaScript 代碼,然后觸發(fā) DOMContentLoaded 事件。

defer 不會改變 script 中代碼的執(zhí)行順序,示例代碼會按照 1、2、3 的順序執(zhí)行。所以,defer 與相比普通 script,有兩點(diǎn)區(qū)別:載入 JavaScript 文件時不阻塞 HTML 的解析,執(zhí)行階段被放到 HTML 標(biāo)簽解析完成之后。

async

<script src="app.js" async></script>

<script src="ad.js" async></script>

<script src="statistics.js" async></script>


async 屬性表示異步執(zhí)行引入的 JavaScript,與 defer 的區(qū)別在于,如果已經(jīng)加載好,就會開始執(zhí)行——無論此刻是 HTML 解析階段還是 DOMContentLoaded 觸發(fā)之后。需要注意的是,這種方式加載的 JavaScript 依然會阻塞 load 事件。換句話說,async-script 可能在 DOMContentLoaded 觸發(fā)之前或之后執(zhí)行,但一定在 load 觸發(fā)之前執(zhí)行。

從上一段也能推出,多個 async-script 的執(zhí)行順序是不確定的。值得注意的是,向 document 動態(tài)添加 script 標(biāo)簽時,async 屬性默認(rèn)是 true,下一節(jié)會繼續(xù)這個話題。

document.createElement

使用 document.createElement 創(chuàng)建的 script 默認(rèn)是異步的,示例如下。

console.log(document.createElement("script").async); // true

所以,通過動態(tài)添加 script 標(biāo)簽引入 JavaScript 文件默認(rèn)是不會阻塞頁面的。如果想同步執(zhí)行,需要將 async 屬性人為設(shè)置為 false。

如果使用 document.createElement 創(chuàng)建 link 標(biāo)簽會怎樣呢?

const style = document.createElement("link");

style.rel = "stylesheet";

style.href = "index.css";

document.head.appendChild(style); // 阻塞?


其實(shí)這只能通過試驗(yàn)確定,已知的是,Chrome 中已經(jīng)不會阻塞渲染,F(xiàn)irefox、IE 在以前是阻塞的,現(xiàn)在會怎樣我沒有試驗(yàn)。

document.write 與 innerHTML

通過 document.write 添加的 link 或 script 標(biāo)簽都相當(dāng)于添加在 document 中的標(biāo)簽,因?yàn)樗僮鞯氖?document stream(所以對于 loaded 狀態(tài)的頁面使用 document.write 會自動調(diào)用 document.open,這會覆蓋原有文檔內(nèi)容)。即正常情況下, link 會阻塞渲染,script 會同步執(zhí)行。不過這是不推薦的方式,Chrome 已經(jīng)會顯示警告,提示未來有可能禁止這樣引入。如果給這種方式引入的 script 添加 async 屬性,Chrome 會檢查是否同源,對于非同源的 async-script 是不允許這么引入的。

如果使用 innerHTML 引入 script 標(biāo)簽,其中的 JavaScript 不會執(zhí)行。當(dāng)然,可以通過 eval() 來手工處理,不過不推薦。如果引入 link 標(biāo)簽,我試驗(yàn)過在 Chrome 中是可以起作用的。另外,outerHTML、insertAdjacentHTML() 應(yīng)該也是相同的行為,我并沒有試驗(yàn)。這三者應(yīng)該用于文本的操作,即只使用它們添加 text 或普通 HTML Element。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容