渲染樹的形成原理你真的很懂嗎?
- 2019 年 10 月 30 日
- 筆記
瀏覽器系列文章

說一下為什麼寫這個系列?
- 原因一:該文章系列不管你是前端開發者,還是後端開發者在面試中經常會被問到一個問題
「從瀏覽器輸入url到頁面顯示經歷了哪些?」一個非常常見的問題,看了該系列絕對能驚到面試官,可能就因為這一道面試題就收了你呢!嘿嘿。 - 原因二:自己主要是後端方向,該系列文章也是為了學習記錄,方便以後查閱。極客時間李兵老師也開了這個專欄,看後還有幾個疑問的點,自己查詢資料學習整理一遍。
什麼是DOM
DOM是Document Object Model(文檔對象模型)的縮寫
W3C 文檔對象模型 (DOM) 是中立於平台和語言的介面,它允許程式和腳本動態地訪問和更新文檔的內容、結構和樣式。-這是W3Cschool給的概念
看了上面的概念好像太「官方」,解釋就是 DOM 是對 HTML 文檔結構化的表述,後端伺服器返回給瀏覽器渲染引擎的 HTML 文件位元組流是無法直接被瀏覽器渲染引擎理解的,要轉化為渲染器引擎可以理解的內部結構,這個結構就是 DOM。W3C 那個概念我好像還沒有把它全部翻譯完,「允許程式和腳本動態地訪問和更新文檔的內容、結構和樣式」。這裡其實就是DOM的作用了
- 頁面展示: DOM 是生成頁面的基礎數據結構
- JavaScript 腳本操作: DOM 提供給 JavaScript 腳本操作的介面,JavaScript 可以通過這些介面對 DOM 結構進行訪問,從而改變文檔的結構和樣式
- 安全: DOM 是一道安全防線,DOM 解析階段會過濾掉一些不安全的 DOM 內容。
本文我主要以 Webkit 渲染引擎來講解,Safari 和 Chrome 都使用 Webkit。Webkit 是一款開源渲染引擎,它本來是為 linux 平台研發的,後來由 Apple 移植到 Mac 及 Windows 上。
渲染樹最終形成經歷了哪些
先看一張整體的流程圖

下面圍繞這張圖和不同代表性對例子進行講解。
HTML解析器
從後端返回給瀏覽器渲染引擎 HTML 文件位元組流, 第一步要經過的就是渲染引擎中的 HTML 解析器。它實現了將 HTML 位元組流轉換為 DOM樹 結構。HTML 文件位元組流返回的過程中 HTML 解析器就一直在解析,邊載入邊解析哦(這裡注意下,有些文章寫的有問題)。
例子1:最簡單的不帶 CSS 和 JavaScript 的 HTML 程式碼講解 HTML 解析器
<html><body> <div>程式設計師成長指北</div></body></html>
根據這段程式碼具體分析 HTML 解析器做了哪些事
階段一 位元組流轉換為字元並W3C標準令牌化
讀取 HTML 的原始位元組流,並根據文件的指定編碼(例如 UTF-8)將它們轉換成各個字元。並將字元串轉換成 W3C HTML5 標準規定的各種令牌,例如,「」、「」,以及其他尖括弧內的字元串。每個令牌都具有特殊含義和一組規則。
一堆位元組流 bytes
3C 62 6F ...
轉成正常的html文件
<html><body> <div> koala <p> 程式設計師成長指北 </P> </div></body></html>
階段二 通過分詞器將位元組流轉化為 Token
分詞器將位元組流轉換為一個一個的 Token,Token 分為 Tag Token和文本 Token,上面這段程式碼最後分詞器轉化後的結果是:

階段三和階段四 將 Token 解析為 DOM 節點,並將 DOM 節點添加到 DOM 樹中
HTML 解析器維護了一個 Token 棧結構(數據結構真是個好東西),這個棧結構的目的就是用來計算節點間的父子關係,在上一個階段生成的 Token 會被順序壓到這個棧中,以下是具體規則:
- HTML 解析器開始工作時,會默認創建了一個根為 document 的空 DOM 結構,同時會將一個 StartTag document 的 Token 壓入棧底。

- 如果壓入到棧中的 StartTagToken,HTML 解析器會為該 Token 創建一個 DOM節點,然後將這個 Dom節點加入到 DOM樹中,它的
父節點就是棧中相鄰的那個元素生成的 DOM節點

- 如果分詞器解析出來的是文本 Token,那麼會生成一個文本節點,然後把這個文本 Dom 節點加入到 DOM 樹中(注:文本Token不需入棧),它的
父節點就是當前棧頂 Token 所對應的 DOM 節點。

- 如果分詞器解析出來的是 EndTag 標籤,比如例子中的 EndTag div,HTML 解析器會查看 Token棧頂的元素是否是 StartTag div,如果是,就將 StartTag div從棧中彈出,邊上該 div 元素解析完成。

- 最後按照上面的規則,分詞器一路解析下來,就形成了這個簡單的 DOM 樹。

此時應該搞懂了核心圖中 HTML 解析器的部分,和 DOM 樹的基本繪製流程,但是現實很殘酷,哪裡有這麼簡單的前端程式碼,還有有 JavaScript 和 CSS 呢!繼續往下看
CSS解析器
CSS 解析器最終的目的也是構建樹不過它構建的樹是 CSSOM 樹 樹的構建流程和 DOM 樹的構建流程基本相同

還是那張圖,具體我就不一一講解一遍了。直接用這個簡單例子
body { font-size: 16px }div { font-weight: bold }div p { display: none }
看下最後構造的 CSSOM 樹

CSSOM 為何具有樹結構?為頁面上的任何對象計算最後一組樣式時,瀏覽器都會先從適用於該節點的最通用規則開始(例如,如果該節點是 body 元素的子項,則應用所有 body 樣式),然後通過應用更具體的規則(即規則「向下級聯」)以遞歸方式優化計算的樣式。
以上面的 CSSOM 樹為例進行更具體的闡述。span 標記內包含的任何置於 body 元素內的文本都將具有 16 像素字型大小,並且顏色為紅色 — font-size 指令從 body 向下級聯至 span。不過,如果某個 span 標記是某個段落 (p) 標記的子項,則其內容將不會顯示。
注意點:
- CSS解析可以與DOM解析同進行
- 如果只有 CSS 和 HTML 的頁面,CSS 不會影響 DOM 樹的創建,但是如果頁面中還有 JavaScript,結論就不一樣了,請繼續往下看。
javascript對DOM樹與CSSOM樹創建的影響
上面兩個例子中都還沒有javascript的出現,接下來說下JavaScript 對 DOM 樹和 CSSOM 樹構建的影響。
- 情況1:當前頁面中只有 Html 和 JavaScript,而且 JavaScript 非外部引入
DOM 樹構建時當遇到JavaScript腳本,就要暫停 DOM 解析,先去執行 Javascript,因為在JavaScript可能會操作當前已經生成的DOM節點。
有一點需要注意:javascript是可能操作當前已經生成的DOM節點,如果是後 面還未生成的DOM節點是不生效的,比如這段程式碼:
<html> <body> <div>1</div> <script> let div1 = document.getElementsByTagName('div')[0] div1.innerText = '程式設計師成長指北' let div2 = document.getElementsByTagName('div')[1] div2.innerText = 'kaola'</script> <div>test</div> </body> </html>
顯示結果為兩行:
第一行結果是程式設計師成長指北 第二行記過是test 因為在執行第三行和第四行 script 腳本的時候,DOM樹中還沒有生成第二個 div對應的dom節點。
- 情況2:當頁面中同時有Html JavaScript CSS ,而且都非外部引入 DOM 樹構建時當遇到 JavaScript 腳本,就要暫停 DOM 解析,先去執行 JavaScript,同時 JavaScript 還要判斷 CSSOM 是否解析完成,因為在 JavaScript 可能會操作 CSSOM 節點,CSSOM 節點確認解析完成,執行 JavaScript 再次回到 DOM 樹創建。(所以這裡也可以理解為CSS解析間接影響DOM樹創建)
- 情況3:當頁面中同時有Html,JavaScript, CSS ,而且外部引入 Webkit渲染引擎有一個優化,當渲染進程接收HTML文件位元組流時,會先開啟一個預解析執行緒,如果遇到 JavaScript 文件或者 CSS 文件,那麼預解析執行緒會提前下載這些數據。當渲染進程接收 HTML 文件位元組流時,會先開啟一個預解析執行緒,如果遇到 JavaScript 文件或者 CSS 文件,那麼預解析執行緒會提前下載這些數據。DOM樹在創建過程中如果遇到JavaScript文件,接下來就和情況2類型一樣了。
影響關係圖: 畫了一張影響關係圖希望大家更好的記憶:

構建渲染樹
通過 DOM 樹和 CSSOM 樹,瀏覽器就可以通過二者構建渲染樹了。瀏覽器會先從 DOM 樹的根節點開始遍歷每個可見節點,然後對每個可見節點找到適配的CSS樣式規則並應用。具體的規則有以下幾點需要注意:
- Render Tree和DOM Tree不完全對應。
- 請注意 visibility: hidden 與 display: none 是不一樣的。前者隱藏元素,但元素仍佔據著布局空間(即將其渲染成一個空框),而後者 (display: none) 將元素從渲染樹中完全移除,元素既不可見,也不是布局的組成部分
看一下前文中提到的 DOM 樹和 CSSOM 樹最終合成的渲染樹結果是:

本文渲染樹形成過程可以做哪些優化
看完了渲染樹的形成,在開發過程中我們能做哪些優化?(注意這裡的優化只是針對渲染樹形成部分,其他的優化會在系列文章之後繼續講)
- 在引入順序上,CSS 資源先於 JavaScript 資源。樣式文件應當在 head 標籤中,而腳本文件在 body 結束前,這樣可以防止阻塞的方式。
- 盡量減少在 JavaScript 中進行DOM操作。
- 簡化並優化CSS選擇器,盡量將嵌套層減少到最小。
總結
看完這篇文章趕緊檢測一下你寫的前端程式碼,腦補一下渲染樹形成過程,想想自己程式碼有沒有需要改善的地方,系列文章會繼續分享,下篇該系列文章渲染樹的布局與繪製以及虛擬DOM樹出現的必要性,感謝觀看。
參考文章
極客時間瀏覽器專欄 瀏覽器渲染原理 https://srtian96.gitee.io/blog/2018/06/01/瀏覽器渲染原理/

深入理解Node.js 進程與執行緒(8000長文徹底搞懂)
require時,exports和module.exports的區別你真的懂嗎?
交流學習
大家好,我是koala,公眾號「程式設計師成長指北」作者。公眾號為您打造優質Node與前端學習路線,並且會推送超級優質文章。加入我們一起學習吧!部落格地址:https://github.com/koala-coding/goodBlog
