博客 / 詳情

返回

瀏覽器渲染原理

瀏覽器是如何渲染頁面的

當瀏覽器的網絡線程收到 HTML 文檔後,會產生一個渲染任務,並將其傳遞給渲染主線程的消息隊列。在事件循環機制下,渲染主線程取出消息隊列中的渲染任務,開啓渲染流程。

BrowserNetworkAndRender.png

整個渲染流程分為多個階段:HTML 解析、樣式計算、佈局、分層、繪製、分塊、光柵化、畫。每個階段都有明確的輸入輸出,上一個階段的輸出就是下一個階段的輸入,整個流程類似流水線一樣。

BrowserRenderFullProgress.png

下面針對每個階段做詳細的研究。

一、解析 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 樹中

HTMLParse-CSSOMDOM.png

HTMLParse-CSSOM.png

QA:

  1. 為什麼需要解析成 DOM、CSSOM?HTML 本身是字符串,無法操作,抽象成一層 JS 文檔對象模型,並暴露一些 API 給 開發者,方便去操作。
  2. 為什麼 HTML、CSS 都有自己對應的 DOM、CSSOM,但 JS 沒有自己對應的模型?因為 CSS、HTML 是需要經常變化的,後續的邏輯會經常操作,所以有自己對應的樹形模型,JS 執行一次就結束了。

HTML 解析過程中遇到 CSS 怎麼處理

為了提高解析效率,瀏覽器會啓動一個預解析器率先下載和解析 CSS

HTMLParse-CSSPreLoad.png

包含哪些樣式

HTML 解析會生成 DOM、CSSOM。StyleSheetList 是所有樣式的集合,那包含哪些樣式呢?

  • 瀏覽器默認樣式(比如 div 獨自佔一行)
  • 內聯樣式
  • 內部樣式
  • 外部樣式
    瀏覽器默認樣式怎麼體現。翻閲 Chromium 的源代碼可以查看到瀏覽器為 html 設置的默認樣式,針對 div 標籤設置了 display: block 所以才獨佔一行,而不是因為是 div 所以就該佔一行。

Chromium-BrowserDefaultStyle.png

HTML 解析過程中遇到 JS 代碼怎麼辦

HTMLParse-JSDownload.png

瀏覽器渲染主線程遇到默認的 script 標籤(不帶 async、defer 修飾)則會暫停解析 HTML,等待 JS 下載並執行完畢後方可繼續解析 HTML。

預解析線程可以分擔下載 JS 的任務。

QA:為什麼 JS 不能設計成像 css 那樣一邊解析,一邊執行呢?
JS 可能會操作當前 DOM,DOM 不是全部的 HTML 解析完才創建的,解析多少 HTML 創建多少 DOM,所以需要暫停 HTML 解析(類似生產者消費者模型,不加鎖則可能存在問題。HTML 解析就是在生產 DOM,JS 腳本可能一邊在讀取 DOM,可能還在增、刪 DOM 在消費 DOM)

為什麼瀏覽器遇到 script 會暫停解析?

瀏覽器在解析 HTML 的時候,如果遇到一個沒有任何屬性的 script 標籤,就會暫停 HTML 解析,先發送網絡請求獲取該 JS 腳本的內容,然後讓 JS 引擎執行該代碼。 當 JS 執行完畢後恢復 HTML 的解析,整個過程如下

HTMLParse-JSDefaultBehavior.png

解決方式:defer async 都是異步加載的外部 JS 腳本,不會阻塞頁面的解析,因此這2個方案都可以規避因為大量 JS 下載影響 HTML 解析的問題

async

async 表示異步,當瀏覽器遇到帶有 async 的 script 時,瀏覽器會採用異步的方式去解析,不會阻塞瀏覽器解析 HTML,一旦網絡請求完成後,如果此時 HTML 還沒解析完的話,立即暫停 HTML 的解析工作,讓 JS 引擎執行 JS 腳本,全部 JS 執行完畢後再去解析後面的 HTML。

HTMLParse-JSAsync1.png
如果遇到 async 修飾的 js 腳本,在異步下載好之後 HTML 已經解析完畢,也一樣,直接執行 JS

HTMLParse-JSAsync2.png

defer

defer 也表示異步延遲,當瀏覽器遇到帶有 defer 的 script 時,會異步獲取該腳本,不會阻塞瀏覽器解析 HTML,一旦網絡請求回來後,如果 HTML 解析還未完成,則會繼續解析 HTML,直到 HTML 全部解析完成後才執行 JS 代碼。
HTMLParse-JSDefer.png

如果存在多個 defer 標籤修飾的 script 標籤,瀏覽器會按照 script 順序執行,不會破壞腳本之間的依賴關係

二、樣式計算

主線程會遍歷得到的 DOM 樹,依次為樹中的每個節點計算出最終的樣式,稱為 Computed Style。
在這一過程中,很多預設值會變成絕對值,比如 red 會變為 rgb(255, 0, 0),相對單位也會變成絕對單位,比如 em 變成 px。這一步完成後會得到一顆帶有樣式的 DOM 樹。

StyleCompute.png
什麼叫計算後的樣式(Computed Style):指的是元素在渲染時所採用的最終樣式。
樣式計算主要包括:CSS 屬性值的計算過程(層疊、繼承...)、視覺格式化模型(盒模型、包含塊、BFC...)

這個計算涉及到:

  • 確定聲明值
  • 層疊衝突
  • 使用繼承
  • 使用默認值

CSS 屬性計算過程


<div>

    <p>p</p>

</div>

沒有修改p 標籤的樣式,卻可以看到他有一個顏色、字體等樣式。
該元素上會有 css 的所有屬性。在 Chrome 審查元素模式下,勾選 Computed 下的 Show all,就可以看到非常多的屬性如下圖:
CSSStyleComputeDemo.png

也就是説:我們開發的任何一個 HTML 元素,實際上在瀏覽器顯示出來的時候都有一套完整的 css 樣式。如果沒有顯示聲明樣式,大概率會採用默認值。

確定聲明值


p {

    color: red;

}

<div>

    <h1>H1標題</h1>

    <p>段落</p>

</div>

css 代碼明確了 p 標籤使用紅色,那麼瀏覽器展示就會按照此屬性進行展示。

這種開發者寫的代碼,叫做作者樣式表。一般瀏覽器還會存在用户代理樣式表,可以認為是瀏覽器內置了一份樣式表。

ComputedStyleDemo-p.png

可以看到 p 標籤在審查元素模式下,除了作者樣式表中設置的 color 屬性,其他的都採用了用户代理樣式表的屬性。比如:display...

層疊衝突

前面看了確定聲明值,但可能存在一種情況,聲明的樣式規則發生衝突。
此時會進入解決層疊衝突的流程,分為3個步驟:

  1. 比較源的重要性
  2. 比如優先級
  3. 比較次序

比較源的重要性

當不同的 css 樣式來源擁有相同的聲明時,此時就會根據樣式表來源的重要性來確定該用哪一條樣式規則。那,有幾種樣式表來源:

  • 瀏覽器會有一個基礎的樣式表來給任何網頁設置默認的樣式。該樣式被稱為:用户代理樣式
  • 網頁的作者可以定義文檔的樣式,這是最常見的樣式,被稱為:頁面作者樣式
  • 瀏覽器的用户,可以使用自定義樣式表定製使用體驗。被稱為用户樣式

重要性的順序為:頁面作者樣式 > 用户樣式 > 用户代理樣式
假設現在有頁面作者樣式表和用户代理樣式表中存在屬性衝突,那會以頁面作者樣式表優先。

p {
    color: red;
    display: inline-block;
}
<div>
    <h1>H1標題</h1>
    <p>段落</p>
</div>

可以看到 p 標籤的 display 屬性,在頁面作者樣式表和用户代理樣式表中同時存在時,根據源的重要性判斷,最終頁面作者樣式表優先級更高,display 採用頁面作者樣式表中的屬性值。

CssStyleDeclarationPriority.png

比較優先級

如果同一個源中,樣式聲明一樣的情況下,如何決策?此時進入了樣式聲明的優先級比較。


.container p {

    color: #00ff00;

}

p {

    color: red;

    display: inline-block;

}

<div class="container">

    <h1>H1標題</h1>

    <p>段落</p>

</div>

可以看到,在上面的 css 代碼中,都在頁面作者樣式表同一個源中,源一樣,解下去根據選擇器的權重來比較重要性。
根據選擇器權重規則(ID 選擇器 > 類選擇器 > 類型選擇器)來判斷,單獨一個標籤選擇器的 color 屬性會被打敗。

關於選擇器的比較策略可以查看 MDN CSS Specificity

CssStyleDeclarationPriority.png

比較次序

當樣式表同源,權重相同的情況下,進入第三階段:比較聲明的順序


.container p {

    color: #00ff00;

}

.container p {

    color: #0000ff;

}

<div class="container">

    <h1>H1標題</h1>

    <p>段落</p>

</div>

都處於頁面作者樣式表中,選擇器的權重也相同,但根據所處位置的不同,下面的樣式聲明會覆蓋上面的值,最終採用 #0000ff。

CSSStylePositionPriority.png
樣式聲明衝突的情況解決了

使用繼承

層疊衝突這一步完成後,解決了相同元素被聲明的多條樣式規則命中後,到底該採用哪一條樣式規則的問題。
那假設一個元素沒有聲明的屬性,該使用什麼屬性值?會存在繼承這個策略。


.container {

    color: #00ff00;

}

<div class="container">

    <h1>H1標題</h1>

</div>

只對類名為 container color 進行了設置,針對 p 標籤沒有任何的設置,但由於 color 可以繼承,所以 p 就從最近的 div 繼承了顏色。

CSSStyleInherited.png

看另一個現象


.container {

    color: blue;

}

.innerContainer {

    color: red;

}

<div class="container">

    <div class="innerContainer">

        <h1>H1標題</h1>

    </div>

</div>

CSSStyleDoubleInherited.png
這裏繼承了 container、innerContainer 2個的屬性值,説了繼承會選擇更近的一個,innerContainer 勝出。

使用默認值

如果前面的步驟都走了,但屬性值還沒確定下來,就只能選用默認值了。


<div>

    <h1>H1標題</h1>

</div>

CSSStyleDefaultValue.png
任何一個元素要在瀏覽器上渲染出來,必須具備所有的 css 屬性值,但很多屬性我們沒有去設置,用户代理樣式表中也沒有設置,也無法從繼承中拿到,因此最終都是使用默認值的。

三、佈局 - layout

佈局完成後會得到佈局樹。
佈局階段會依次遍歷 DOM 樹的每一個節點,計算每個節點的幾何信息,比如節點的寬高、相對包含塊的位置。
元素的尺寸和位置,會受它的包含塊所影響。對於一些屬性,例如 width, height, padding, margin,絕對定位元素的偏移值(比如 position 被設置為 absolute 或 fixed),當我們對其賦予百分比值時,這些值的計算值,就是通過元素的包含塊計算得來。

Browser-Layout.png

大部分時候,DOM 樹和 Layout 樹並非一一對應的,為什麼?

CSSLayoutTreeDisplay.png

比如某個節點的 display 設置為 none,這樣的節點就沒有幾何信息,因此不會生成到 layout 樹。有些使用了偽元素選擇器,雖然 DOM 樹中並不存在這些偽元素節點,但它們需要顯示在瀏覽器上,所以會生成到 layout 樹上。還有匿名行盒、匿名塊盒等等都會導致 DOM 樹和 layout 樹無法一一對應。

CSSLayoutTreePseudoClass.png


div::before {

    content: '';

}

<div></div>

CSSPseudoClassLayout.png

四、分層 Layer

瀏覽器拿到佈局後的 layout tree 會思考一些事情。大多數頁面不是繪製後靜止的,經常會有一些動畫、用户點擊後一些交互處理等,需要經常刷新頁面。但如果整個頁面直接重新刷新,這個性能開銷是很大的。能不能提高效率?能,於是現在瀏覽器支持了分層。

主線程會使用一套複雜的策略對整個佈局樹進行分層。
分層的好處在於,將來在某一個層改變後,僅會對該層進行後續處理,從而提高效率。

Browser-Layer.png

滾動條、堆疊上下文有關的(z-index、transform、opacity )樣式都會或多或少影響分層結果,也可以通過 will-change 屬性更大程度的影響分層的結果。


<div>

    100個<p>Lorem</p>

</div>

Browser-Layer-Demo.png

可以看到默認情況下瀏覽器對該代碼分了2層。為什麼滾動條需要單獨設置一個 layer?因為100個 p 標籤一定會超出一屏,所以會存在滾動條,且用户可能會高頻滾動去查看內容,為了高效渲染,所以將滾動條單獨設置了一個層。

假設我們已經通過技術手段得知某些內容經常會改變,但直接頁面整體重刷會很耗費資源,那如何只對某一塊區域設置一個 layer 呢?通過 will-change: transform 告訴瀏覽器,這塊區域的 transform 屬性經常會變。

will-change是一個用於通知瀏覽器某個元素即將發生變化的CSS屬性。它可以被應用到任何元素上,用於提前告知瀏覽器該元素將要有哪些屬性進行改變,從而優化渲染性能。

通過在元素上設置will-change屬性,開發者可以明確指示瀏覽器對該元素進行優化處理。這樣一來,瀏覽器可以提前分配資源和準備工作,以便在實際改變發生之前進行相應的合成操作。這樣做有助於避免不必要的重繪和重排,提高頁面的響應速度和動畫的流暢度。

will-change屬性可以接受多個屬性值,表示將要改變的屬性。例如,will-change: transform表示元素即將進行變形操作,will-change: opacity表示元素的透明度即將發生變化。

需要注意的是,will-change屬性應在實際變化發生前的一段時間內被設置,以便瀏覽器有足夠的時間進行準備和優化。另外,will-change屬性並不會自動觸發硬件加速,但它可以為瀏覽器提供一種優化渲染的提示。

此外,will-change屬性的使用應謹慎,避免濫用。只有在明確知道元素即將發生某種變化,並且這種變化對性能有影響時,才應使用will-change屬性。過度使用will-change屬性可能會導致瀏覽器進行不必要的優化,反而降低性能

總的來説,will-change屬性是一個用於優化渲染性能的CSS屬性,通過提前告知瀏覽器元素即將發生的變化,使瀏覽器能夠提前進行準備和優化,從而提高頁面的響應速度和動畫的流暢度

Demo:


div {

    will-change: transform;

    width: 200px;

    background-color: red;

    margin: 0 auto;

}

通過上面的 css 設置,告訴瀏覽器:這個 div 的 transform 經常會變,瀏覽器會為該元素創建一個獨立的圖層,將這個圖層標記為“即將變換”。這樣,在進行佈局和繪製時,瀏覽器就可以更高效地處理這個元素,而無需重新計算整個渲染樹。
Browser-Layer-Demo2.png

五、繪製 - Paint

Browser-Paint.png

第四步的產出就是分層。第五步繪製階段,會分別對每個層單獨產生繪製指令集,用於描述這一層的內容該如何畫出來(類似 canvas 代碼,告訴計算機從哪個點經過中間幾個點,最後到哪個點繪製一條線,中間內容用什麼顏色填充)

渲染主線程的工作到此為止,剩餘步驟交給其他線程完成。

Browser-MainThread-duty.png

六、分塊 - Tiling

完成繪製之後,主線程將每個圖層的繪製信息提交給合成線程,剩餘的工作將由合成線程完成。

Browser-Tilling.png

合成線程首先對每個圖層分塊(Tiling),將其劃分為更多的小區域。合成線程類似一個任務調度者,它會從線程池中拿出多個線程來完成分塊的工作。

Browser-Tilling-Worker.png

QA:瀏覽器渲染過程中分塊的作用是什麼?

  1. 提高渲染性能:當瀏覽器需要渲染一個複雜的網頁時,如果一次性將所有內容加載並渲染出來,可能會消耗大量的計算資源和時間,通過網頁分成多個較小的塊(tiling)瀏覽器可以並行地加載和渲染這些塊,從而充分利用計算資源,提高渲染性能。
  2. 優化內存佔用:如果瀏覽器一次性加載和渲染整個網頁,可能會消耗大量的內存。通過將網頁分成多個圖塊,瀏覽器可以更好的管理內存,避免不必要的內存消耗
  3. 實現懶加載:通過分塊,瀏覽器可以實現懶加載,即只有當某個圖塊進入視口可見區域時,才開始加載和渲染該圖塊。這樣可以減少不必要的加載和渲染,提高頁面的加載速度和性能
  4. 方便進行並行處理和異步操作:通過將網頁分成多個圖塊,瀏覽器可以更容易地進行並行處理和異步操作。例如,在移動端設備上,可以利用 GPU 進行圖塊的並行渲染,提高渲染效率。

分塊 tiling 技術是瀏覽器渲染過程中提高性能、優化內存使用和實現懶加載的重要手段之一。

七、光柵化

分塊完成後會進入光柵化階段。光柵化是將每個塊變成位圖。
Browser-Raster.png
合成線程將分塊後的塊信息交給 GPU 進程,以極高的速度完成光柵化,並優先處理靠近視口的塊。
Browser-Raster-GPU.png

八、畫 - Draw

合成線程計算出每個位圖在屏幕上的位置,交給 GPU 進行最終的呈現。

Browser-Draw.png

合成線程拿到每個層(Layer)、每個塊(Tile)的位圖(bitmap)後,生成一個個指引(quad)信息。指引信息標識出每個位圖應該畫到屏幕上的哪個位置,此過程會考慮到旋轉、縮放、變形等。

因為變形發生在合成線程,與渲染主線程無關,這也是 transform 效率高的本質原因。
合成線程會把 quad 提交給 GPU 進程,由 GPU 進程產生系統調用,提交給 GPU 硬件,完成最終的屏幕顯示

完成過程如下

Browser-Full-Progress.png

reflow

Browser-Reflow.png
比如某個業務邏輯,導致我們用 js 腳本修改了某個節點的寬度、顏色,這其實修改的是 cssom,假設業務邏輯導致操作了 DOM,DOM 節點增刪了,cssom、DOM 改變了,則會觸發一系列的的流程,比如重新計算樣式 style、佈局(layout)、分層 layer、繪製 paint、分塊 tiling、光柵化 raster、畫 draw、系統調用 GPU 去真正顯示。

當渲染樹 render tree 中的一部分(或者全部)因為元素的尺寸、佈局、顏色、隱藏等改變而需要重新構建,這就情況被稱為迴流。圖上的 style 這部分。

迴流發生時,瀏覽器會讓渲染樹中受到影響的部分失效,並重新構建這部分渲染樹。完成迴流 reflow 後,瀏覽器會重新繪製受影響的部分到屏幕中,該過程稱為重繪。

簡單來説,reflow 就是計算元素在屏幕上確切的位置和大小並重新繪製。迴流的代價遠大於重繪。迴流必定重繪,重繪不一定迴流。

repaint

當渲染樹 render tree 中的一些元素需要更新樣式,但這些樣式只是改變元素外觀、風格,而不影響佈局(位置、大小),則叫重繪。
簡單來説,重繪就是將渲染樹節點轉換為屏幕上的實際像素,不涉及重新佈局階段的位置與大小計算

QA:為什麼 transform 效率高?
假設在 css 中針對某個元素寫了 transform: rotate(100deg),transform 既不會影響佈局也不會影響繪製指令,它影響的是渲染流程的最後一個階段,圖上的 draw 階段。由於 draw 階段發生在合成線程中,不在渲染主線程,所以 transform 的任何變化幾乎不會影響主線程。同樣,不管主線程如何繁忙,都不影響合成線程中 transform 的變化。

Browser-Reflow.png

user avatar yueqiushangdezhentou 頭像
1 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.