從輸入URL到頁面展示,這中間發生了那些過程(2)?
當網路程序將資源提交給渲染程序的時候,此時渲染程序就要開始渲染頁面了。瀏覽器的渲染機制是十分複雜的,所以渲染會被劃分為很多子階段,輸入的HTML、CSS、JS以及圖片等資源經過這些階段,最終輸出畫素展示到頁面上,我們把瀏覽器的這樣一個處理流程叫做渲染流水線。
渲染流水線
按照渲染的時間順序,我們把瀏覽器的渲染時間段分為以下幾個子階段,需要注意的是每一個子階段的輸出都會當做下一個子階段的輸入,這也符合實際的工廠流水線的場景。
- 構建DOM樹
- 構建CSSOM樹
- 佈局階段
- 分層
- 繪製
- 分塊
- 光柵化
- 合成
構建DOM樹
在詳細瞭解瀏覽器是如何構建DOM樹之間,我們先思考下瀏覽器為什麼需要構建DOM樹?
這是因為瀏覽器作為一款軟體,它的排版引擎決定了它並不能直接識別HTML檔案,而是需要先經過Parse HTML階段,也就是先將HTML檔案中的一個個標籤解析為瀏覽器可以理解一個個DOM樹節點,然後才能進行後續的渲染工作。
流程:輸入HTML —————— Parse HTML —————— 輸出DOM樹
如何檢視瀏覽器解析HTML產生的DOM樹?
在console控制檯輸入document回車,就可以看到瀏覽器解析之後完整的DOM樹結構。DOM樹結構和HTML幾乎是一樣的,唯一的區別是瀏覽器為DOM提供了可供js增刪改查的介面。
樣式計算 Recaculate Style生成CSSOM樹也就是styleSheets
上一步已經得到了DOM樹結構,但是每一個節點的具體樣式還無法確定,所以這一階段主要的任務就是進行樣式計算,計算出每一個dom節點具體的css樣式,這個階段總共分三步進行:
1. 將CSS文字轉化成瀏覽器理解的styleSheets
和瀏覽器解析HTML檔案的原因一樣,瀏覽器自身也是不能直接理解CSS文字樣式的,所以瀏覽器渲染引擎在解析CSS檔案的時候,會將CSS文字先轉化為瀏覽器可以理解的結構,也就是styleSheets。
如何檢視瀏覽器解析CSS產生的styleSheets?
在console控制檯中輸入document.styleSheets,便可以看到瀏覽器解析之後的樣式表。和解析HTML產生的樹結構不同的時候,瀏覽器解析CSS文字會得到一個列表(document.styleSheets的值是一個數組列表StyleSheetList)。列表裡面每一項都是一個CSSStyleSheet這個類構造出來的物件,這種陣列結構同時具備了查詢和修改的功能,為後續JS來操作樣式打下基礎。
StyleSheetList:[
0:CSSStyleSheet{
cssRules: CSSRuleList {0: CSSStyleRule, 1: CSSStyleRule}
disabled: false
href: null
media: MediaList {length: 0, mediaText: ''}
ownerNode: style
ownerRule: null
parentStyleSheet: null
rules: CSSRuleList {0: CSSStyleRule, 1: CSSStyleRule, 2: CSSStyleRule, length: 3}
title: null
type: "text/css"
[[Prototype]]: CSSStyleSheet
},
1:CSSStyleSheet{...},
2:CSSStyleSheet{...}
]
2. 將樣式表中的非標屬性值轉化為瀏覽器可以理解的標準屬性值
上一步我們已經拿到了將CSS文字轉化後對應的StyleSheets結構了,接下來就要對其進行屬性值的標準化操作。
具體來說就是我們日常寫的css樣式中有一些屬性值其實是不標準的,這些屬性值難以被渲染引擎所直接理解,比如:
div{
display:none;
color:red;
font-size:2rem;
font-weight:bold;
}
像red這種顏色值、bold、rem這種單位都不是渲染引擎容易理解的標準化的計算值,在樣式計算之前會有一個將其標準化的過程:
div{
display:none;
color:rgb(0,0,255);
font-size:32px;
font-weight:700;
}
3. 計算出DOM樹中每個節點的具體樣式並儲存在ComputedStyle屬性中
經過前面兩步,此時樣式的屬性都是標準化的值,此時就可以開始進行樣式計算,為每一個DOM節點計算出其具體的樣式了。樣式計算的核心是遵循CSS的兩大核心規則:層疊和繼承。
CSS繼承規則:子元素會繼承父元素的某些CSS屬性,如果父元素也沒有宣告,那麼會繼承自瀏覽器的預設樣式user agent stylesheet。在樣式計算的過程中,瀏覽器會基於DOM節點的繼承關係來合理計算樣式
CSS層疊規則:對於一個DOM節點來說,如果該節點有來自多個源的屬性值,那麼如何確定哪些屬性值會被最終應用到節點身上生效,這其中的演算法就是CSS的層疊規則。
樣式計算階段會計算出每一個DOM節點的具體樣式,在計算的過程中會遵守CSS的層疊和繼承兩個規則,並把最終每個DOM節點的樣式儲存在該節點的ComputedStyle中。在chrome瀏覽器中任意選中一個標籤,然後點選右側的ComputedStyle,就可以獲取到該節點計算後的全部樣式。
關於如何用JS獲取某個DOM節點的全部計算樣式
function getEleStyle(ele:HTMLElement,attr:string){
/* IE瀏覽器 */
if(ele.currentStyle){
return ele.currentStyle[attr];
}else{
/* Chrome瀏覽器 */
return window.getComputedStyle(ele,null)[attr];
}
}
eg:
const app = document.querySelector('#app');
const appStyle = getEleStyle(app,'color');
佈局階段:形成佈局樹Layout Tree
現在有了DOM樹和每個DOM節點的樣式資訊,還需要知道每一個DOM元素在頁面的幾何位置資訊,那麼接下來需要計算出每一個DOM樹中可見元素的幾何位置,我們把這個階段稱之為佈局階段。
1. 建立佈局樹Layout Tree,過濾掉所有非可見節點
為了構建最終的佈局樹,瀏覽器會遍歷DOM樹上的所有可見節點,並將這些節點依次新增到佈局樹當中。需要注意的是:
- 一開始生成的DOM樹中有一些節點最終並不會被渲染展示,比如head標籤下的所有內容都不會被渲染;
- 其次在進行CSS樣式計算的時候,會有一些節點的display為none,所以這些節點也不會被新增進最終的佈局樹中。
2. 佈局計算
接下來就開始計算佈局樹中每一個節點具體的幾何位置座標資訊了,在佈局的過程中瀏覽器會將佈局計算的結果又寫回到佈局樹中,此時佈局樹中就儲存了每一個節點的幾何座標資訊
分層階段:對佈局樹進行分層,形成分層樹
在渲染程序真正渲染頁面之前,還需要對頁面上一些複雜的效果進行處理,比如3D旋轉,頁面滾動或者是使用z-index做z軸上的排序,為了方便實現這些效果,渲染引擎還會為一些特殊的節點生成專用的圖層,並最終生成一顆圖層樹,也叫作LayerTree。((Layer Compositor:生成圖層)
在Chrome的開發者工具中,有一個Layers標籤就可以檢視整個頁面的分層情況。可以看到渲染引擎為頁面上的不同節點分了不同的層,最終和PS一樣這些圖層疊加後合成頁面。
哪些元素會在渲染的時候被單獨分為一層?
-
擁有層疊上下文屬性的元素會被提升為單獨的一層
開啟了定位屬性的元素:position:fiexed
定義了透明屬性的元素 opacity:0.5
使用了css濾鏡的元素:filter:blue(5px)
定位元素聲明瞭z-index屬性 -
當文字超出盒子邊界產生裁剪的時候,文字部分會單獨提升為一個層
一旦div盒子中文字超出盒子的最大邊界而產生滾動的時候,文字就會被單獨提升為一個層
div{
width:50px;
height:50px;
overflow:auto;
}
圖層繪製:為每個圖層生成待繪製指令列表,並commit給合成執行緒
在完成圖層樹的構建之後,渲染引擎會對圖層樹中的每個圖層進行繪製。渲染引擎繪製圖層的時候,會把每一個圖層的繪製分解為多個小的繪製指令,然後這些繪製指令按照繪製的順序組成一個待繪製列表,隨著時間按照指令依次繪製。
檢視瀏覽器繪製圖層的流程:
- 開啟Chrome的Layers標籤,選中左側的document
- Details標籤代表等待繪製的指令列表
- 右側Profiler頁面代表繪製圖層的時間軸
- 然後拖動右側的時間軸,檢視隨著時間的進行頁面的繪製過程
柵格化raster操作:將圖層基於視口劃分為圖塊,並將圖塊柵格化為點陣圖
繪製列表只是用來記錄繪製順序和繪製指令的列表,而實際上繪製操作是由渲染引擎中的合成執行緒來完成的。當上一步的圖層繪製指令列表準備好之後,渲染主執行緒會把該繪製指令列表提交給合成執行緒。
為什麼要用合成執行緒?避免一次繪製過多頁面 而是分塊進行繪製
當一個頁面內容很多的時候,瀏覽器不可能一次性將所有內容全部繪製完成,而是隻繪製使用者視口區域的一部分頁面呈現給使用者,那麼如何實現呢?
合成執行緒的主要工作就是將大的圖層劃分為小的圖塊,一般圖塊的大小是256或者512px,然後合成執行緒會按照當前視口附近的圖塊來優先生成點陣圖,實際生成點陣圖的操作是由GPU顯示卡程序中的柵格化來加速執行的。
什麼是柵格化?將圖塊轉化為點陣圖
柵格化就是將圖塊轉化為點陣圖的操作,圖塊是柵格化執行的最小單元,渲染程序會準備一個專門供柵格化的執行緒池,所有圖塊的柵格化都是線上程池中執行的。
通常,柵格化過程都會使用 GPU 來加速生成,使用 GPU 生成點陣圖的過程叫快速柵格化,或者 GPU 柵格化,生成的點陣圖被儲存在 GPU 記憶體中。
合成和展示
-
合成執行緒傳送DrawQuad命令給瀏覽器主程序
渲染程序的合成執行緒接收到圖層的繪製訊息時,會通過光柵化執行緒池將其提交給GPU程序,在GPU程序中執行光柵化操作,執行完成,再將結果返回給渲染程序的合成執行緒,執行合成圖層操作!
此時合成執行緒就會生成一個繪製圖塊的命令——“DrawQuad”,然後將該命令提交給瀏覽器程序。 -
瀏覽器程序接收到DrawQuad命令將頁面展示到顯示器上
瀏覽器程序裡面有一個叫 viz 的元件,用來接收合成執行緒發過來的 DrawQuad 命令,然後根據 DrawQuad 命令,瀏覽器程序裡會執行顯示合成(Display Compositor),也就是將所有的圖層合成為可以顯示的頁面圖片。 最終顯示器顯示的就是瀏覽器程序中合成的頁面圖片
重排、重繪和合成操作對於渲染流水線的影響
1. 重排reflow:更新了元素的幾何屬性
如果我們通過javascript和css修改了一個元素的幾何位置屬性,比如改變元素的寬度和高度,那麼這種操作會觸發瀏覽器的重新佈局,也就是要重新佈局計算每個元素的幾何位置,然後進行分層和圖層繪製等之後一系列的子階段,由於這種操作會重新更新完整的渲染流水線,所以對瀏覽器渲染的開銷最大,我們把這種操作叫做重排。
2. 重繪repaint:更新了元素的繪製屬性
如果我們通過js或者css修改了一個元素的繪製屬性,比如顏色、背景顏色等,這種操作只是修改了元素的外觀,並沒有引起元素幾何位置的變化,所以不用進行新的佈局和分層階段,而是直接進入渲染流水線的繪製階段,所以相對於重排來說這種操作的開銷相對要小一點。
3. 直接合成:更新一個既不是幾何又不是繪製的屬性
如果我們用新增的css3屬性比如transform屬性來實現了一個動畫效果,那麼這種操作既不會修改元素的幾何屬性也不會修改元素的繪製屬性,這時候渲染程序會在非主執行緒上開直接執行合成動畫操作,這中間直接跳過了佈局和繪製階段,也不會佔用渲染主執行緒的資源,所以這種操作是最節省效能的。
這也就告訴我們在為一個元素實現樣式變化的時候,儘可能的用css3的transform屬性來執行變化,因為這樣不會觸發重排和重繪,執行效率最高。