目錄
- 渲染時間點
- 渲染流水線
- 1,解析(parse)HTML
- 1.1,DOM樹
- 1.2,CSSOM樹
- 1.3,解析時遇到 css 是怎么做的
- 1.4,解析時遇到 js 是怎么做的
- 2,樣式計算 Recalculate style
- 3,布局 layout
- 4,分層 layer
- 5,繪制 paint
- 6,分塊 tiling
- 7,光柵化 raster
- 8,畫 draw
- 常見面試題
- 什么是 reflow
- 什么是 repaint
- 為什么 transform 效率高
在上一篇文章中,介紹了 瀏覽器的事件循環,其中提到了瀏覽器的進程模型。那瀏覽器是如何渲染頁面的呢?
渲染時間點
瀏覽器會通過網絡進程中的線程來通信,獲取到 html 數據后生成渲染任務,發送給消息隊列。
渲染主線程會執行渲染任務。整個渲染流程:把 html 字符串解析為像素點信息,再交給 GPU來渲染后在頁面中展示。
渲染流水線
每個階段都有明確的輸入輸出,上個階段的輸出會成為下個階段的輸入。形成一套完整的流水線。
1,解析(parse)HTML
會將 html 字符串解析為 2個樹。因為字符串不好操作,對象更容易處理。
1.1,DOM樹
也就是 document
對象。可以在控制臺通過console.dir(document)
展示對象結構。
1.2,CSSOM樹
包括:
<style>
內部樣式<link>
外部樣式style=""
內聯樣式- 瀏覽器默認樣式表
注意,這里的 CSSOM樹 ≠ document.styleSheets
。因為 document.styleSheets
只包括內部樣式和外部樣式,每寫一個 <style>
或 <link>
就會多一個 CSSStyleSheet
樣式表:
舉例說明:
<html><head><style>body h1 {color: red;font-size: 3em;}div p {margin: 1em;color: blue;}</style></head><body><h1>下雪天的夏風</h1><div><p>求關注</p></div></body>
</html>
可以看到:
CSSStyleSheet
樣式表CSSStyleRule
規則對象selectorText
選擇器style
樣式(鍵值對)
另外,CSSStyleSheet
樣式表是可以直接通過 js 操作的。
舉例:通過 js 給頁面所有 div 添加 border
document.styleSheets[0].addRule('div', 'border: 1px solid !important')
這樣添加樣式的方式,一般框架用的多。最終樣式不會在內聯樣式中展現。
1.3,解析時遇到 css 是怎么做的
渲染主線程遇到 css 時,會啟動一個預解析線程,讓它來率先下載和解析 css。渲染主線程繼續解析 html。
預解析線程會快速瀏覽,如果遇到外部樣式link
,會通知網絡線程來下載 css,下載完成后進行“解析”完成后交給渲染主線程。
并不是真正的解析,只是做一些前期工作,最終生成 CSSOM 樹還是由渲染主線程來完成。
所以,css 代碼不會阻塞解析 HTML。
1.4,解析時遇到 js 是怎么做的
沒有生成所謂的 js 樹,是因為 js 只需要執行一遍即可。DOM樹和CSSOM樹作為解析 HTML 的輸出,后續還會有其他的操作。
渲染主線程遇到內部 js 時,直接啟動 V8 引擎執行即可;遇到外部 js 時,會啟動一個預解析線程,讓它來下載 js,渲染主線程暫停。
預解析線程會通知網絡線程來下載 js,下載完成后再交給渲染主線程來執行。執行完繼續解析 HTML。
這樣做的原因:DOM 樹是邊解析邊生成的,而 js 代碼可能會修改之前已解析好的內容。
所以,js 代碼會阻塞解析 HTML。
2,樣式計算 Recalculate style
遍歷DOM樹,計算每個節點的最終樣式 Computed Style。
這一過程,許多預設值會變成絕對值,比如 red
變為 rgb(255,0,0)
;相對單位變為絕對單位,比如rem
變為 px
最終會得到1個帶有最終樣式的 DOM 樹。
可以在瀏覽器的 computed 窗口中,或使用 getComputedStyle() 查看某個元素的最終樣式。
3,布局 layout
遍歷DOM樹的每個節點,根據 css 屬性值計算每個節點的幾何信息(尺寸,相對包含塊的位置),生成一個 layout 樹。
注意到 DOM 樹和 layout 樹不一定一一對應。
舉例1:diaplay:none
的元素不會出現在 layout 樹中。
問題來了,為什么<head>
<link>
等元素默認是隱藏的?因為在瀏覽器默認樣式表中,它們 diaplay:none
!
舉例2:偽元素的 content
內容在 DOM 樹中沒有,在 layout 樹中有。
舉例3:內容必須在行盒中,行盒和塊盒不能相鄰。所以在 layout 樹中會有匿名塊盒。
解釋:“行級元素”,“塊級元素”這些元素指的是 html。但元素的類型是 css 屬性決定的。所以稱為行盒或塊盒。
4,分層 layer
現在 layout 布局樹中每個節點的幾何信息,尺寸位置等都明確了。渲染主線程會使用一套策略對整個布局樹分層。
目的是提升效率,這樣可以讓之后頁面的修改更新不會影響到其他層。類似 PS 中的圖層,修改某一個圖層不會影響到其他圖層。
可以在瀏覽器控制臺的
Layers
面板查看當前網頁的分層信息。
也不會分太多的層,因為會比較占內存。滾動條是單獨一層。
另外,和堆疊上下文有關的 css 屬性(transform,opacity)會影響分層的決策。其中 will-change
屬性能更大程度的影響分層角色,可能會將設置該屬性的元素單獨分一層。
因為這個屬性會告訴瀏覽器,我可能會經常變化,瀏覽器最好掂量下。
5,繪制 paint
分層后,會對每層都生成繪制指令,類似于 canvas 中的 API 一樣。其實canvas 用的就是瀏覽器內核的繪制功能。
指令舉例:“筆”移動到 xx 坐標位置,畫 100*100 的矩形,并用紅色填充等等。
以上。渲染主線程的工作結束,剩下的步驟交給其他線程來完成。
6,分塊 tiling
將每層都分為多個小的區域,瀏覽器視窗區域的優先級最高,靠近視窗區域的優先級次之。
分塊邏輯:渲染主線程每個圖層的繪制信息交給合成線程,合成線程又會啟動多個分塊線程來對每個圖層進行分塊。
合成線程也屬于渲染線程
7,光柵化 raster
將每個塊變成位圖,位圖就是每個像素點的信息。優先處理靠近視窗的塊。
位圖就是內存中的二位數組,其中記錄了每個像素點的信息。
此過程會用到GPU來加速,用到顯卡。
8,畫 draw
合成線程現在拿到了所有的信息,在畫之前還需要確認【指引信息 quad】,也就是位圖相對的屏幕在哪里。
注意,之前布局樹中的信息是相對于整個頁面的。現在需要知道每個塊相對于屏幕的位置信息。
步驟:合成線程將指引信息 --> GPU 進程 --> 硬件顯卡,由顯卡來呈現最終的像素信息。
GPU 做中轉的原因是:GPU 是瀏覽器的進程。合成線程屬于渲染進程,它是在沙盒中的,與硬件系統做隔離,提升安全性。所以渲染進程是沒有調度系統硬件能力的。
而 css 中的 transform 屬性就是在這一步完成的。這就是 transform 效率高的原因,直接跳過之前所有的步驟。
常見面試題
什么是 reflow
【recalculate layoutBlockFlow 重排】它的本質是重新計算 layout 樹。
當做了會影響 layout 樹的操作后,比如修改幾何尺寸相關的信息時,會引起重新計算 layout 樹。
并且,為了避免連續多次的操作導致 layout 樹反復計算,瀏覽器會合并這些操作,當 js 代碼完成后統一計算。所以這一步驟是異步的。
同樣因為這個原因,當 js 獲取布局屬性時,可能無法獲取到最新的布局信息。
瀏覽器會在反復權衡下,最終決定獲取屬性時立即 reflow。
什么是 repaint
它的本質是重新根據分層信息計算了繪制指令。
當改變了可見樣式,比如顏色等和幾何尺寸無關的屬性時,就需要重新計算,會從【繪制】這一步驟開始重新執行。
而因為幾何尺寸也屬于可見樣式,所以 reflow 一定會引起 repaint。
為什么 transform 效率高
因為 transform 既不會影響布局,也不會影響繪制,它只會影響渲染的最后一步【畫】。而【畫】是在合成線程中,不會影響到渲染主線程。同樣無論渲染主線程多忙,也不會影響到 transform。
驗證代碼如下,當渲染主線程卡死時,transform 不受影響。
<!DOCTYPE html>
<html lang="en"><head><style>.common {width: 50px;height: 50px;background-color: salmon;border-radius: 50%;margin: 10px;}.ball1 {animation: move1 1s alternate infinite;}.ball2 {position: relative;left: 0;animation: move2 1s alternate infinite;}@keyframes move1 {to {transform: translate(100px);}}@keyframes move2 {to {left: 100px;}}</style></head><body><button id="btn">死循環3s</button><div class="common ball1"></div><div class="common ball2"></div><script>btn.addEventListener("click", function () {delay(3000);});function delay(duration) {var start = Date.now();while (Date.now() - start < duration) {}}</script></body>
</html>
效果:
滾動也是一樣的邏輯,如果 js 有一段上面的死循環,并不會影響到滾動。因為只有視窗內元素的位置變了,直接執行【畫 draw】這一步驟。
以上。
參考:渡一教育。