瀏覽器的渲染:過程與原理
內容說明
本文不是關於瀏覽器渲染的底層原理或前端優化具體細節的講解,而是關於瀏覽器對頁面的渲染——這一過程的描述及其背後原理的解釋。這是因為前端優化是一個非常龐大且零散的知識集合,一篇文章如果要寫優化的具體方法恐怕只能做一些有限的列舉。
然而,如果瞭解清楚瀏覽器的渲染過程、渲染原理,其實就掌握了指導原則。根據優化原則,可以實現出無數種具體的優化方案,各種預編譯、預載入、資源合併、按需載入方案都是針對瀏覽器渲染習慣的優化。
關鍵渲染路徑
提到頁面渲染,有幾個相關度非常高的概念,最重要的是關鍵渲染路徑,其他幾個概念都可以從它展開,下面稍作說明。
關鍵渲染路徑(Critical Rendering Path)是指與當前使用者操作有關的內容。例如使用者剛剛開啟一個頁面,首屏的顯示就是當前使用者操作相關的內容,具體就是瀏覽器收到 HTML、CSS 和 JavaScript 等資源並對其進行處理從而渲染出 Web 頁面。
瞭解瀏覽器渲染的過程與原理,很大程度上是為了優化關鍵渲染路徑,但優化應該是針對具體問題的解決方案,所以優化沒有一定之規。例如為了保障首屏內容的最快速顯示,通常會提到漸進式頁面渲染,但是為了漸進式頁面渲染,就需要做資源的拆分,那麼以什麼粒度拆分、要不要拆分,不同頁面、不同場景策略不同。具體方案的確定既要考慮體驗問題,也要考慮工程問題。
瀏覽器渲染頁面的過程
從耗時的角度,瀏覽器請求、載入、渲染一個頁面,時間花在下面五件事情上:
- DNS 查詢
- TCP 連線
- HTTP 請求即響應
- 伺服器響應
- 客戶端渲染
本文討論第五個部分,即瀏覽器對內容的渲染,這一部分(渲染樹構建、佈局及繪製),又可以分為下面五個步驟:
- 處理 HTML 標記並構建 DOM 樹。
- 處理 CSS 標記並構建 CSSOM 樹。
- 將 DOM 與 CSSOM 合併成一個渲染樹。
- 根據渲染樹來佈局,以計算每個節點的幾何資訊。
- 將各個節點繪製到螢幕上。
需要明白,這五個步驟並不一定一次性順序完成。如果 DOM 或 CSSOM 被修改,以上過程需要重複執行,這樣才能計算出哪些畫素需要在螢幕上進行重新渲染。實際頁面中,CSS 與 JavaScript 往往會多次修改 DOM 和 CSSOM,下面就來看看它們的影響方式。
阻塞渲染:CSS 與 JavaScript
談論資源的阻塞時,我們要清楚,現代瀏覽器總是並行載入資源。例如,當 HTML 解析器(HTML Parser)被指令碼阻塞時,解析器雖然會停止構建 DOM,但仍會識別該指令碼後面的資源,並進行預載入。
同時,由於下面兩點:
- 預設情況下,CSS 被視為阻塞渲染的資源,這意味著瀏覽器將不會渲染任何已處理的內容,直至 CSSOM 構建完畢。
- JavaScript 不僅可以讀取和修改 DOM 屬性,還可以讀取和修改 CSSOM 屬性。
存在阻塞的 CSS 資源時,瀏覽器會延遲 JavaScript 的執行和 DOM 構建。另外:
- 當瀏覽器遇到一個 script 標記時,DOM 構建將暫停,直至指令碼完成執行。
- JavaScript 可以查詢和修改 DOM 與 CSSOM。
- CSSOM 構建時,JavaScript 執行將暫停,直至 CSSOM 就緒。
所以,script 標籤的位置很重要。實際使用時,可以遵循下面兩個原則:
- CSS 優先:引入順序上,CSS 資源先於 JavaScript 資源。
- JavaScript 應儘量少影響 DOM 的構建。
瀏覽器的發展日益加快(目前的 Chrome 官方穩定版是 61),具體的渲染策略會不斷進化,但瞭解這些原理後,就能想通它進化的邏輯。下面來看看 CSS 與 JavaScript 具體會怎樣阻塞資源。
CSS
<style> p { color: red; }</style>
<link rel="stylesheet" href="index.css">
這樣的 link 標籤(無論是否 inline)會被視為阻塞渲染的資源,瀏覽器會優先處理這些 CSS 資源,直至 CSSOM 構建完畢。
渲染樹(Render-Tree)的關鍵渲染路徑中,要求同時具有 DOM 和 CSSOM,之後才會構建渲染樹。即,HTML 和 CSS 都是阻塞渲染的資源。HTML 顯然是必需的,因為包括我們希望顯示的文字在內的內容,都在 DOM 中存放,那麼可以從 CSS 上想辦法。
最容易想到的當然是精簡 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)">
第一個資源會載入並阻塞。 第二個資源設定了媒體型別,會載入但不會阻塞,print 宣告只在列印網頁時使用。 第三個資源提供了媒體查詢,會在符合條件時阻塞渲染。
JavaScript
JavaScript 的情況比 CSS 要更復雜一些。觀察下面的程式碼:
<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 標籤會阻塞 HTML 解析,無論是不是 inline-script。上面的 P 標籤會從上到下解析,這個過程會被兩段 JavaScript 分別打斷一次(載入並且執行的時間段內)。
所以實際工程中,我們常常將資源放到文件底部。
改變阻塞模式:defer 與 async
為什麼要將 script 載入的 defer 與 async 方式放到後面呢?因為這兩種方式是的出現,全是由於前面講的那些阻塞條件的存在。換句話說,defer 與 async 方式可以改變之前的那些阻塞情形。
首先,注意 async 與 defer 屬性對於 inline-script 都是無效的,所以下面這個示例中三個 script 標籤的程式碼會從上到下依次執行。
<!-- 按照從上到下的順序輸出 1 2 3 -->
<script async>
console.log("1");
</script>
<script defer>
console.log("2");
</script>
<script>
console.log("3");
</script>
故,下面兩節討論的內容都是針對設定了 src 屬性的 script 標籤。
defer
<script src="app1.js" defer></script>
<script src="app2.js" defer></script>
<script src="app3.js" defer></script>
defer 屬性表示延遲執行引入的 JavaScript,即這段 JavaScript 載入時 HTML 並未停止解析,這兩個過程是並行的。整個 document 解析完畢且 defer-script 也載入完成之後(這兩件事情的順序無關),會執行所有由 defer-script 載入的 JavaScript 程式碼,然後觸發 DOMContentLoaded 事件。
defer 不會改變 script 中程式碼的執行順序,示例程式碼會按照 1、2、3 的順序執行。所以,defer 與相比普通 script,有兩點區別:載入 JavaScript 檔案時不阻塞 HTML 的解析,執行階段被放到 HTML 標籤解析完成之後。
async
<script src="app.js" async></script>
<script src="ad.js" async></script>
<script src="statistics.js" async></script>
async 屬性表示非同步執行引入的 JavaScript,與 defer 的區別在於,如果已經載入好,就會開始執行——無論此刻是 HTML 解析階段還是 DOMContentLoaded 觸發之後。需要注意的是,這種方式載入的 JavaScript 依然會阻塞 load 事件。換句話說,async-script 可能在 DOMContentLoaded 觸發之前或之後執行,但一定在 load 觸發之前執行。
從上一段也能推出,多個 async-script 的執行順序是不確定的。值得注意的是,向 document 動態新增 script 標籤時,async 屬性預設是 true,下一節會繼續這個話題。
document.createElement
使用 document.createElement 建立的 script 預設是非同步的,示例如下。
console.log(document.createElement("script").async); // true
所以,通過動態新增 script 標籤引入 JavaScript 檔案預設是不會阻塞頁面的。如果想同步執行,需要將 async 屬性人為設定為 false。
如果使用 document.createElement 建立 link 標籤會怎樣呢?
const style = document.createElement("link");
style.rel = "stylesheet";
style.href = "index.css";
document.head.appendChild(style); // 阻塞?
其實這隻能通過試驗確定,已知的是,Chrome 中已經不會阻塞渲染,Firefox、IE 在以前是阻塞的,現在會怎樣我沒有試驗。
document.write 與 innerHTML
通過 document.write 新增的 link 或 script 標籤都相當於新增在 document 中的標籤,因為它操作的是 document stream(所以對於 loaded 狀態的頁面使用 document.write 會自動呼叫 document.open,這會覆蓋原有文件內容)。即正常情況下, link 會阻塞渲染,script 會同步執行。不過這是不推薦的方式,Chrome 已經會顯示警告,提示未來有可能禁止這樣引入。如果給這種方式引入的 script 新增 async 屬性,Chrome 會檢查是否同源,對於非同源的 async-script 是不允許這麼引入的。
如果使用 innerHTML 引入 script 標籤,其中的 JavaScript 不會執行。當然,可以通過 eval() 來手工處理,不過不推薦。如果引入 link 標籤,我試驗過在 Chrome 中是可以起作用的。另外,outerHTML、insertAdjacentHTML() 應該也是相同的行為,我並沒有試驗。這三者應該用於文字的操作,即只使用它們新增 text 或普通 HTML Element。
參考資料
Mobile Analysis in PageSpeed Insights
Web Fundamentals
MDN - HTML element reference
相關文章
- 瀏覽器渲染過程與原理淺析(一)瀏覽器
- 瀏覽器渲染過程與效能優化瀏覽器優化
- 瀏覽器渲染網頁的過程瀏覽器網頁
- 瀏覽器渲染原理瀏覽器
- 【瀏覽器】渲染原理探究瀏覽器
- WebKit 瀏覽器內幕之 瀏覽器特性/網頁渲染過程WebKit瀏覽器網頁
- 瀏覽器的渲染原理簡介瀏覽器
- 瀏覽器渲染原理及流程瀏覽器
- 一張圖瞭解瀏覽器渲染頁面的過程瀏覽器
- 瀏覽器渲染頁面過程簡單介紹瀏覽器
- 深入淺出瀏覽器渲染原理瀏覽器
- 瀏覽器渲染原理(一文搞懂)瀏覽器
- 瀏覽器渲染瀏覽器
- 問我Chrome瀏覽器的渲染原理(6000字長文)Chrome瀏覽器
- 瀏覽器渲染流程瀏覽器
- 瀏覽器渲染引擎瀏覽器
- 從瀏覽器渲染原理談動畫效能優化瀏覽器動畫優化
- 基石-初見瀏覽器(一):瀏覽器渲染瀏覽器
- 【瀏覽器】瀏覽器基本工作原理瀏覽器
- 瀏覽器之渲染引擎瀏覽器
- 瀏覽器渲染簡述瀏覽器
- 瀏覽器渲染機制瀏覽器
- Chrome 瀏覽器頁面渲染工作原理淺析Chrome瀏覽器
- 瀏覽器原理瀏覽器
- 瀏覽器引擎、渲染引擎與JavaScript引擎的區別瀏覽器JavaScript
- 瀏覽器頁面載入過程瀏覽器
- 瀏覽器EventLoop執行過程解析瀏覽器OOP
- 使用 ClojureScript 開發瀏覽器外掛的過程與收穫瀏覽器
- Google瀏覽器Logo的誕生過程Go瀏覽器
- 瀏覽器渲染流水線解析瀏覽器
- 瀏覽器核心渲染:重建引擎瀏覽器
- 瀏覽器和伺服器之前的加密解密過程瀏覽器伺服器加密解密
- 瀏覽器頁面資源載入過程與優化瀏覽器優化
- 瀏覽器頁面渲染機制瀏覽器
- 瀏覽器渲染魔法之合成層瀏覽器
- 瀏覽器效能優化-渲染效能瀏覽器優化
- 必須明白的瀏覽器渲染機制瀏覽器
- 從 Chrome 看瀏覽器的渲染機制Chrome瀏覽器