一.瀏覽器是如何渲染頁面的?
當瀏覽器的網絡線程收到 HTML 文檔後,會產生一個渲染任務,並將其傳遞給渲染主線程的消息隊列。在事件循環機制的作用下,渲染主線程取出消息隊列中的渲染任務,開啓渲染流程。
整個渲染流程分為多個階段,分別是: HTML 解析(Parse HTML)、樣式計算(Computed Style)、佈局(Layout)、分層(Layer)、繪製(Paint)、分塊(Tiling)、光柵化(Raster)、畫(Draw)。
每個階段都有明確的輸入輸出,上一個階段的輸出會成為下一個階段的輸入。這樣,整個渲染流程就形成了一套組織嚴密的生產流水線。
1.HTML 解析(Parse HTML)
瀏覽器解析過程中遇到 CSS 解析 CSS,遇到 JS 執行 JS。為了提高解析效率,瀏覽器在開始解析前,會啓動一個預解析的線程,率先下載 HTML 中的外部 CSS 文件和 外部的 JS 文件。
如果主線程解析到 link 位置,此時外部的 CSS 文件還沒有下載解析好,主線程不會等待,繼續解析後續的 HTML。這是因為下載和解析 CSS 的工作是在預解析線程中進行的。這就是 CSS 不會阻塞 HTML 解析的根本原因。為了提⾼解析效率,瀏覽器會啓動⼀個預解析器率先下載和解析 CSS,如下圖:
如果主線程解析到 script 位置,會停止解析 HTML,轉而等待 JS 文件下載好,並將全局代碼解析執行完成後,才能繼續解析 HTML。這是因為 JS 代碼的執行過程可能會修改當前的 DOM 樹,所以 DOM 樹的生成必須暫停。這就是 JS 會阻塞 HTML 解析的根本原因。如下圖:
第一步完成後,會得到 DOM 樹和 CSSOM 樹,瀏覽器的默認樣式、內部樣式、外部樣式、行內樣式均會包含在 CSSOM 樹中。
2.樣式計算(Computed Style)
主線程會遍歷得到的 DOM 樹,依次為樹中的每個節點計算出它最終的樣式,稱之為 Computed Style。
在這一過程中,很多預設值會變成絕對值,比如red會變成rgb(255,0,0);相對單位會變成絕對單位,比如em會變成px。這一步完成後,會得到一棵帶有樣式的 DOM 樹。
3.佈局(Layout)
佈局階段會依次遍歷 DOM 樹的每一個節點,計算每個節點的幾何信息。例如節點的寬高、相對包含塊的位置。大部分時候,DOM 樹和佈局樹並非一一對應。
比如 display:none 的節點沒有幾何信息,因此不會生成到佈局樹;又比如使用了偽元素選擇器,雖然 DOM 樹中不存在這些偽元素節點,但它們擁有幾何信息,所以會生成到佈局樹中。還有匿名行盒、匿名塊盒等等都會導致 DOM 樹和佈局樹無法一一對應。這裏各個元素的位置是相對於 包含塊 的位置。
包含塊:
英語全稱為containing block,就是元素的尺寸和位置,會受它的包含塊所影響。對於一些屬性,例如 width, height, padding, margin,絕對定位元素的偏移值(比如 position 被設置為 absolute 或 fixed),當我們對其賦予百分比值時,這些值的計算值,就是通過元素的包含塊計算得來。
包含塊分為三種:
1.一種是根元素(HTML 元素)所在的包含塊,被稱之為初始包含塊(initial containing block)。對於瀏覽器而言,初始包含塊的的大小等於視口 viewport 的大小,基點在畫布的原點(視口左上角)。它是作為元素絕對定位和固定定位的參照物。
2.另外一種是對於非根元素,對於非根元素的包含塊判定就有幾種不同的情況了。大致可以分為如下幾種:
- 如果元素的
positiion是relative或static,那麼包含塊由離它最近的塊容器(block container)的內容區域(content area)的邊緣建立。 - 如果
position屬性是fixed,那麼包含塊由視口建立。 - 如果元素使用了
absolute定位,則包含塊由它的最近的position的值不是static(也就是值為fixed、absolute、relative或sticky)的祖先元素的內邊距區的邊緣組成。
3.如果 position 屬性是 absolute 或 fixed,包含塊也可能是由滿足以下條件的最近父級元素的內邊距區的邊緣組成的:
transform或perspective的值不是nonewill-change的值是transform或perspectivefilter的值不是none或will-change的值是filter(只在Firefox下生效).contain的值是paint(例如:contain: paint;)
例子:
<body>
<div class="container">
<div class="item">
<div class="item2"></div>
</div>
</div>
</body>
.container {
width: 500px;
height: 300px;
background-color: skyblue;
position: relative;
}
.item {
width: 300px;
height: 150px;
border: 5px solid;
margin-left: 100px;
}
.item2 {
width: 100px;
height: 100px;
background-color: red;
position: absolute;
left: 10px;
top: 10px;
}
其結果如下:
其實原因也非常簡單,根據上面的第三條規則,對於 div.item2 來講,它的包含塊應該是 div.container,而非 div.item。
4.分層(Layer)
主線程會使用一套複雜的策略對整個佈局樹中進行分層。分層的好處在於,將來某一個層改變後,僅會對該層進行後續處理,從而提升效率。滾動條、堆疊上下文、transform、opacity 等樣式都會或多或少的影響分層結果,也可以通過 will-change 屬性更大程度的影響分層結果。
5.繪製(Paint)
主線程會為每個層單獨產生繪製指令集,用於描述這一層的內容該如何畫出來。
渲染主線程的⼯作到此為⽌,剩餘步驟交給其他線程完成,如下圖:
6.分塊(Tiling)
完成繪製後,主線程將每個圖層的繪製信息提交給合成線程,剩餘工作將由合成線程完成。合成線程首先對每個圖層進行分塊,將其劃分為更多的小區域。它會從線程池中拿取多個線程來完成分塊工作。
7.光柵化(Raster)
光柵化是將每個塊變成位圖,此過程會⽤到 GPU 加速,會開啓多個線程來完成光柵化,並優先處理靠近視⼝的塊。
8.畫(Draw)
合成線程拿到每個層、每個塊的位圖後,生成一個個「指引(quad)」信息。指引會標識出每個位圖應該畫到屏幕的哪個位置,以及會考慮到旋轉、縮放等變形。變形發生在合成線程,與渲染主線程無關,這就是 transform 效率高的本質原因。合成線程會把 quad 提交給 GPU 進程,由 GPU 進程產生系統調用,提交給 GPU 硬件,完成最終的屏幕成像。
整一個渲染過程如下圖:
二.面試題:
1.什麼是 reflow(重排/迴流) ?
答:
reflow 的本質就是重新計算 layout 樹,如元素位置,大小等改變。
⑴當進行了會影響佈局樹的操作後,需要重新計算佈局樹,會引發 layout。
⑵為了避免連續的多次操作導致佈局樹反覆計算,瀏覽器會合並這些操作,當 JS 代碼全部完成後再進行統一計算。所以,改動屬性造成的 reflow 是異步完成的。也同樣因為如此,當 JS 獲取佈局屬性時,就可能造成無法獲取到最新的佈局信息。
⑶瀏覽器在反覆權衡下,最終決定獲取屬性立即 reflow。
2.什麼是 repaint(重繪) ?
答:
repaint 的本質就是重新根據分層信息計算了繪製指令,如元素顏色,背景色等改變。
⑴當改動了可見樣式後,就需要重新計算,會引發 repaint。
⑵由於元素的佈局信息也屬於可見樣式,所以 reflow(重排/迴流) 一定會引起 repaint(重繪)。
3.為什麼 transform 的效率高?
答:
因為 transform 既不會影響佈局也不會影響繪製指令,它影響的只是渲染流程的最後一個「draw」階段。
由於 draw 階段在合成線程中,所以 transform 的變化幾乎不會影響渲染主線程。反之,渲染主線程無論如何忙碌,也不會影響 transform 的變化。