什么是瀏覽器渲染
簡單來說,就是將HTML字符串 —> 像素信息
渲染時間點
瀏覽器什么時候開始渲染?
-
網絡線程發送請求,取回HTML封裝為渲染任務并將其傳遞給渲染主線程的消息隊列。
問題:只取回HTML嗎?那CSS和JS呢?
其實這兩者都在HTML中,分別以link和script引入。
-
在事件循環機制的作用下,「渲染主線程」從消息隊列中取出任務,開始渲染頁面
渲染主線程一步一步處理,最終渲染好整個頁面
整個渲染流程分為多個階段,分別是: HTML 解析、樣式計算、布局、分層、繪制、分塊、光柵化、畫。
每個階段都有明確的輸入輸出,上一個階段的輸出會成為下一個階段的輸入。
這樣,整個渲染流程就形成了一套組織嚴密的生產流水線。
下面詳細講述一下每一步都進行了哪些操作。
渲染頁面的詳細步驟
1.解析HTML
渲染主線程拿到網絡進程下載好的HTML字符串
,將其解析為DOM樹和CSSOM樹。
這兩個樹是C++對象(因為瀏覽器是用C++寫的),不過在C++對象外部套了一層JS,所以我們可以通過JS操作這兩個樹。
為什么要處理成「樹」對象?
因為直接操作HTML字符串太過于復雜,將其變為對象后,直觀且便于操作。
什么是DOM樹
DOM樹的根節點是document,也就是我們平時寫的元素結構。
它本身是一個對象,可以通過console.dir(document)查看
處理為對象后,更加便于操作。
什么是CSSOM樹
和DOM樹類似,不過它是CSS對象,根節點叫做StyleSheetList,它是各種樣式表的集合,比如內部樣式表,外部樣式表,內聯(行內)樣式表, 瀏覽器默認樣式表。多個表對應多個CSStyleSheet。
瀏覽器默認樣式表是什么?
控制臺中,帶有user agent stylesheet標識的就是瀏覽器默認樣式表。
怎么查看全部的瀏覽器默認樣式表?
可以通過github查看Chrome的源碼,這里推薦一個小插件,幫助我們不用下載就可以在vscode快速查看Chrome源碼,這個插件叫做GitHub Repositories。
下載完畢后,可以直接通過url查看Chrome源代碼。
點擊這個圖標登錄綁定github賬號后,直接通過url打開并查看代碼。
打開后,我們可以根據路徑查看瀏覽器默認樣式表。
third_party > blink > renderer > core > html > resources > html.css
JS如何操作CSSOM樹?
常見的方法
dom元素.style
document.styleSheets // 這個數組其實就是CSStyleSheetList
示例,將頁面中所有div添加一個紅色邊框
document.styleSheets[0].addRule('div','border:1px solid red !important')
其實Vue框架的實現就用到了該方法。
解析過程中遇到CSS怎么辦
為了提高解析效率,瀏覽器會啟動一個預解析器率先下載和解析CSS。
預解析器瀏覽速度很快,他只負責找到CSS、JS并通知「網絡線程」,下載完畢后初步解析后交給「渲染主線程」繼續處理。
期間「渲染主線程」如果發現CSS,會直接跳過
,繼續處理后面的HTML,直到「預解析線程」初步解析CSS完畢后,才會繼續處理。
解析過程中遇到JS怎么辦
與CSS不同的是,「渲染主線程」遇到JS代碼時,必須暫停一切行為,等待下載完成后才繼續解析HTML。因為「渲染主線程」無法確定JS中是否改動了之前解析的dom。
2.樣式計算
這一步將DOM結構和CSS合并,計算后得到每一個節點的最終樣式(計算后的樣式)。
其實就是CSS屬性計算過程,例如層疊(權重)、繼承。比如類選擇器和id選擇器中的樣式,需要判斷層疊性。
這一步的過程中很多預設值會變成絕對值,比如顏色red會變成rgb(255,0,0),相對單位會變成絕對單位。
這一步完成后,會得到一棵帶有樣式的DOM樹。
3.布局
這一步「渲染主線程」會遍歷上一步的DOM樹,依次計算出每個節點的大小以及位置。計算完成后的樹叫做Layout樹。
CSS中不是有寬高嗎,為什么還要計算?
因為有的情況元素沒有設置寬高,那么就是auto,會根據其內容計算。
每個節點的位置是根據什么計算的呢?
是根據他父元素嗎?
答案是否定的。其實是根據其「包含塊」元素的位置,比如一個元素是絕對定位,但其父元素并非相對定位,但父元素的父元素是相對定位,那么這個元素的包含塊就是他的爺爺元素。這個元素能根據他父元素計算位置嗎?必然不能,會根據它的爺爺元素。
DOM樹和Layout樹的節點不一定一一對應
有些display為none的元素不會被渲染。
偽元素會新生成一個節點。
內容必須在行盒中,行盒和塊盒不能相鄰。
Layout樹中的每一個節點并不是dom對象,而是C++對象,JS是獲取不到的。但是可以獲取到一些信息,比如常用的document.body.clientWidth等都是從「布局樹」中獲取的。
4.分層
瀏覽器會自動分層,將來某一個層發生改變后,僅對該層進行修改,以確保修改后再次渲染時的高效。
堆疊上下文會影響分層,例如z-index、opacity、transform。
如何查看圖層
-
添加圖層工具
-
展開發現,有很多圖層
代碼無法讓瀏覽器分層,但可以使用給元素添加css,瀏覽器可能會為其設置單獨圖層。
.container{will-change: transform
}
5.繪制
繪制后會為每個圖層生成繪制指令。
「渲染主線程」至此,已經做完了全部操作,其余的步驟交給其他線程。
6.分塊
完成繪制后,「渲染主線程」會將每個涂層的繪制信息提交給「合成線程」,「合成線程」將每一個圖層分成多個小區域。
它會從線程池中拿取多個線程來完成分塊工作。
7.光柵化
將每個塊變成位圖,優先處理靠近視口的塊。
此過程會用到GPU加速
8.畫
合成線程拿到每個位圖在屏幕上的位置,交給GPU完成最終呈現。
完整過程
- 生成DOM樹和CSSOM樹
- 將DOM樹和CSSOM樹融合成新的DOM樹(結構樣式相結合)
- 將DOM樹處理為Layout樹(忽略display:none的元素)
- 將Layout樹分層(修改dom后只重新渲染其所在圖層)
- 繪制每個圖層,用于渲染
- 將繪制好的圖層分塊
- 將每個塊處理成位圖(GPU做的)
- GPU最終「畫」出頁面
常見面試題
什么是reflow
reflow 的本質就是重新計算 layout 樹。
當進行了會影響布局樹的操作后,需要重新計算布局樹,會引發 layout。
為了避免連續的多次操作導致布局樹反復計算,瀏覽器會合并這些操作,當 JS 代碼全部完成后再進行統一計算。所以,改動屬性造成的 reflow 是異步完成的。
也同樣因為如此,當 JS 獲取布局屬性時,就可能造成無法獲取到最新的布局信息。
瀏覽器在反復權衡下,最終決定獲取屬性立即 reflow。
常見操作如幾何尺寸發生改變,CSSOM發生改變,需要重新計算布局樹。
什么是repaint
repaint 的本質就是重新根據分層信息計算了繪制指令。
當改動了可見樣式后,就需要重新計算,會引發 repaint。
由于元素的布局信息也屬于可見樣式,所以 reflow 一定會引起 repaint。
常見操作如顏色發生改變,會跳過計算布局和分層,會比reflow塊。
為什么transform效率高
因為 transform 既不會影響布局也不會影響繪制指令,它影響的只是渲染流程的最后一個「draw」階段
由于 draw 階段在合成線程中,所以 transform 的變化幾乎不會影響渲染主線程。反之,渲染主線程無論如何忙碌,也不會影響 transform 的變化。
總結
當瀏覽器的網絡線程收到 HTML 文檔后,會產生一個渲染任務,并將其傳遞給渲染主線程的消息隊列。
在事件循環機制的作用下,渲染主線程取出消息隊列中的渲染任務,開啟渲染流程。
整個渲染流程分為多個階段,分別是: HTML 解析、樣式計算、布局、分層、繪制、分塊、光柵化、畫
每個階段都有明確的輸入輸出,上一個階段的輸出會成為下一個階段的輸入。
這樣,整個渲染流程就形成了一套組織嚴密的生產流水線。
渲染的第一步是解析 HTML。
解析過程中遇到 CSS 解析 CSS,遇到 JS 執行 JS。為了提高解析效率,瀏覽器在開始解析前,會啟動一個預解析的線程,率先下載 HTML 中的外部 CSS 文件和 外部的 JS 文件。
如果主線程解析到link
位置,此時外部的 CSS 文件還沒有下載解析好,主線程不會等待,繼續解析后續的 HTML。這是因為下載和解析 CSS 的工作是在預解析線程中進行的。這就是 CSS 不會阻塞 HTML 解析的根本原因。
如果主線程解析到script
位置,會停止解析 HTML,轉而等待 JS 文件下載好,并將全局代碼解析執行完成后,才能繼續解析 HTML。這是因為 JS 代碼的執行過程可能會修改當前的 DOM 樹,所以 DOM 樹的生成必須暫停。這就是 JS 會阻塞 HTML 解析的根本原因。
第一步完成后,會得到 DOM 樹和 CSSOM 樹,瀏覽器的默認樣式、內部樣式、外部樣式、行內樣式均會包含在 CSSOM 樹中。
渲染的下一步是樣式計算。
主線程會遍歷得到的 DOM 樹,依次為樹中的每個節點計算出它最終的樣式,稱之為 Computed Style。
在這一過程中,很多預設值會變成絕對值,比如red
會變成rgb(255,0,0)
;相對單位會變成絕對單位,比如em
會變成px
這一步完成后,會得到一棵帶有樣式的 DOM 樹。
接下來是布局,布局完成后會得到布局樹。
布局階段會依次遍歷 DOM 樹的每一個節點,計算每個節點的幾何信息。例如節點的寬高、相對包含塊的位置。
大部分時候,DOM 樹和布局樹并非一一對應。
比如display:none
的節點沒有幾何信息,因此不會生成到布局樹;又比如使用了偽元素選擇器,雖然 DOM 樹中不存在這些偽元素節點,但它們擁有幾何信息,所以會生成到布局樹中。還有匿名行盒、匿名塊盒等等都會導致 DOM 樹和布局樹無法一一對應。
下一步是分層
主線程會使用一套復雜的策略對整個布局樹中進行分層。
分層的好處在于,將來某一個層改變后,僅會對該層進行后續處理,從而提升效率。
滾動條、堆疊上下文、transform、opacity 等樣式都會或多或少的影響分層結果,也可以通過will-change
屬性更大程度的影響分層結果。
再下一步是繪制
主線程會為每個層單獨產生繪制指令集,用于描述這一層的內容該如何畫出來。
完成繪制后,主線程將每個圖層的繪制信息提交給合成線程,剩余工作將由合成線程完成。
合成線程首先對每個圖層進行分塊,將其劃分為更多的小區域。
它會從線程池中拿取多個線程來完成分塊工作。
分塊完成后,進入光柵化階段。
合成線程會將塊信息交給 GPU 進程,以極高的速度完成光柵化。
GPU 進程會開啟多個線程來完成光柵化,并且優先處理靠近視口區域的塊。
光柵化的結果,就是一塊一塊的位圖
最后一個階段就是畫了
合成線程拿到每個層、每個塊的位圖后,生成一個個「指引(quad)」信息。
指引會標識出每個位圖應該畫到屏幕的哪個位置,以及會考慮到旋轉、縮放等變形。
變形發生在合成線程,與渲染主線程無關,這就是transform
效率高的本質原因。
合成線程會把 quad 提交給 GPU 進程,由 GPU 進程產生系統調用,提交給 GPU 硬件,完成最終的屏幕成像。