讓我們來看看主要資料結構,也就是轉譯管道的輸入和輸出內容。
這些資料結構如下:
- 影格樹狀結構由本機和遠端節點組成,這些節點代表哪些網頁文件位於哪個轉譯程序和哪個 Blink 轉譯器中。
- 不可變動的片段樹狀結構代表版面配置限制條件演算法的輸出內容 (以及輸入內容)。
- 屬性樹狀結構代表網頁文件的轉換、剪輯、效果和捲動階層。這些值會在管道中使用。
- 顯示清單和繪圖區塊是轉譯和分層演算法的輸入內容。
- 合成器影格會封裝用於使用 GPU 繪製的介面、轉譯介面和 GPU 紋理圖塊。
在介紹這些資料結構之前,以下範例會建構架構審查中的一個範例。本文件會使用這個範例,說明如何將資料結構套用至該範例。
<!-- Example code -->
<html>
<div style="overflow: hidden; width: 100px; height: 100px;">
<iframe style="filter: blur(3px);
transform: rotateZ(1deg);
width: 100px; height: 300px"
id="one" src="foo.com/etc"></iframe>
</div>
<iframe style="top:200px;
transform: scale(1.1) translateX(200px)"
id="two" src="bar.com"></iframe>
</html>
影格樹狀結構
Chrome 有時會選擇在與父項框架不同的轉譯程序中,轉譯跨來源框架。
在範例程式碼中,總共有三個影格:
在網站隔離模式下,Chromium 會使用兩個轉譯程序來轉譯這個網頁。每個轉譯程序都有該網頁的頁框樹狀圖表示方式:
在不同程序中算繪的框架會以遠端框架的形式顯示。遠端影格會保留在轉譯中充當預留位置所需的最少資訊,例如其尺寸。否則,遠端影格不會包含任何用於轉譯實際內容所需的資訊。
相較之下,當地影格代表經過標準算繪管道的影格。當地影格包含轉換該影格資料 (例如 DOM 樹狀結構和樣式資料) 為可轉譯及顯示的內容所需的所有資訊。
轉譯管道會以本機影格樹狀結構片段的精細程度運作。請參考以下較複雜的範例,其中 foo.com
是主要框架:
<iframe src="bar.com"></iframe>
以及下列 bar.com
子畫面:
<iframe src="foo.com/etc"></iframe>
雖然仍只有兩個轉譯器,但現在有三個本機影格樹狀結構片段,其中兩個位於 foo.com
的轉譯程序中,另一個則位於 bar.com
的轉譯程序中:
為了為網頁產生一個合成器影格,Viz 會同時從三個本機影格樹狀結構的根影格要求一個合成器影格,然後匯總這些影格。另請參閱合成器影格部分。
foo.com
主頁框和 foo.com/other-page
子頁框屬於同一個頁框樹狀結構,且會在同一個程序中轉譯。不過,由於這兩個影格屬於不同的本機影格樹狀結構片段,因此仍有獨立的文件生命週期。因此,無法在單一更新中為兩者產生一個合成器影格。算繪程序沒有足夠的資訊,無法將為 foo.com/other-page
產生的合成器影格直接合成 foo.com
主影格的合成器影格。舉例來說,外部程序 bar.com
父項影格可能會影響 foo.com/other-url
iframe 的顯示方式,方法是使用 CSS 轉換 iframe,或在 DOM 中使用其他元素遮蔽 iframe 的部分。
視覺屬性更新瀑布
裝置縮放比例和可視區大小等視覺屬性會影響算繪輸出結果,且必須在本機影格樹狀結構片段之間同步。每個本機影格樹狀結構片段的根目錄都會與相關聯的小工具物件建立關聯。視覺屬性更新會先傳送至主框架的小工具,然後再由上至下傳播至其他小工具。
舉例來說,當可視區域大小變更時:
這個程序並非即時完成,因此複製的視覺屬性也會包含同步化權杖。Viz 影像合成器會使用這個同步符記,等待所有本機影格樹狀圖片段使用目前的同步符記提交影像合成器影格。這項程序可避免將不同視覺屬性的合成器影格混合。
不可變動的片段樹狀結構
不可變動的片段樹狀結構是轉譯管線版面配置階段的輸出內容。它代表網頁上所有元素的位置和大小 (未套用轉換)。
每個片段都代表 DOM 元素的一部分。通常每個元素只有一個片段,但如果在列印時分散到不同頁面,或是在多欄內容中分散到不同欄,則可能會有更多片段。
版面配置完成後,每個區段都會變成不可變更,且不會再變更。重要的是,我們也設下了一些額外限制。我們不會:
- 允許樹狀結構中的任何「上」參照。(子項無法擁有指向父項的指標)。
- 將資料「泡泡」向下傳遞至樹狀結構 (子項只會讀取子項的資訊,而不會讀取父項的資訊)。
這些限制可讓我們在後續版面配置中重複使用片段。沒有這些限制,我們就必須經常重新產生整個樹狀結構,這會造成不必要的開銷。
大多數版面配置通常是漸進式更新,例如網頁應用程式會在使用者按一下元素時,更新 UI 的一小部分。理想情況下,版面配置只應根據螢幕上實際變更的內容執行作業。我們可以透過盡可能重複使用先前樹狀結構的部分來達成這點。也就是說,我們通常只需要重建樹狀結構的脊椎。
日後,這項不可變動的設計可讓我們執行有趣的操作,例如在需要時跨執行緒邊界傳遞不可變動的片段樹狀結構 (以便在不同執行緒上執行後續階段)、產生多個樹狀結構以便流暢的版面配置動畫,或執行並行推測版面配置。這也讓我們有機會使用多執行緒版面配置。
內嵌片段項目
內嵌內容 (主要是格式文字) 會使用略有不同的表示方式。我們會在代表樹狀結構的平面清單中,呈現內嵌內容,而非使用樹狀結構中的方塊和指標。主要優點是,內嵌式平面清單表示法速度快,可用於檢查或查詢內嵌式資料結構,且記憶體效率高。這對網頁轉譯效能而言非常重要,因為文字轉譯作業相當複雜,除非經過高度最佳化,否則很容易成為管道中最慢的部分。
系統會依照內嵌版面配置子樹的深度優先搜尋順序,為每個內嵌格式設定內容建立平面清單。清單中的每個項目都是 (物件、後代數量) 的元組。舉例來說,請考慮以下 DOM:
<div style="width: 0;">
<span style="color: blue; position: relative;">Hi</span> <b>there</b>.
</div>
width
屬性已設為 0,因此文字會在「Hi」和「there」之間換行。
當此情況的內嵌格式設定內容以樹狀結構呈現時,會如下所示:
{
"Line box": {
"Box <span>": {
"Text": "Hi"
}
},
"Line box": {
"Box <b>": {
"Text": "There"
}
},
{
"Text": "."
}
}
扁平清單如下所示:
- (線條方塊,2)
- (Box <span>, 1)
- (文字「Hi」,0)
- (線條方塊,3)
- (Box <b>, 1)
- (文字「there」,0)
- (文字「.」, 0)
這個資料結構有許多使用者,包括無障礙 API 和幾何 API,例如 getClientRects
和 contenteditable
。每個國家/地區的需求都不同。這些元件會透過方便的游標存取平面資料結構。
游標包含 MoveToNext
、MoveToNextLine
和 CursorForChildren
等 API。這項游標表示法對於文字內容非常實用,原因如下:
- 以深度優先搜尋順序進行疊代作業的速度非常快。這類操作非常常見,因為它與插入符號移動類似。由於這是平面清單,深度優先搜尋只會遞增陣列偏移量,提供快速疊代和記憶體區域。
- 它提供橫向搜尋功能,這項功能在繪製線條和內嵌方塊的背景時非常必要。
- 只要知道子項數量,就能快速移至下一個同胞節點 (只要將陣列偏移量增加該數量即可)。
資源樹
DOM 是元素 (加上文字節點) 的樹狀結構,CSS 可為元素套用各種樣式。
這會以四種方式顯示:
- 版面配置:輸入至版面配置限制演算法。
- Paint:如何繪製及轉成點陣圖元素 (但不包括其子項)。
- 視覺:套用至 DOM 子樹的算繪/繪圖效果,例如轉換、濾鏡和裁剪。
- 捲動:軸對齊和圓角裁剪,以及包含子樹狀結構的捲動。
屬性樹狀結構是資料結構,可說明視覺效果和捲動效果如何套用至 DOM 元素。這些元素可用來回答以下問題:在給定的 DOM 元素版面配置大小和位置下,該元素相對於螢幕的位置為何?以及:應使用哪個 GPU 操作序列來套用視覺和捲動效果?
網路上的視覺效果和捲動效果非常複雜。因此,屬性樹狀結構最重要的功能,就是將複雜性轉換為單一資料結構,以便精確呈現其結構和含義,同時移除 DOM 和 CSS 的其他複雜性。這讓我們可以更有信心地實作合成和捲動演算法。請特別注意以下幾點:
- 可將可能容易出錯的幾何圖形和其他計算集中於單一位置。
- 建構及更新資源樹狀結構的複雜度會分離至一個轉譯管道階段。
- 相較於完整的 DOM 狀態,將屬性樹狀結構傳送至不同執行緒和程序會更簡單、更快速,因此可用於多種用途。
- 用途越多,我們就能從頂層建立的幾何圖形快取中獲得越多優勢,因為這些用途可以重複使用彼此的快取。
RenderingNG 會將屬性樹狀結構用於許多用途,包括:
- 將合成作業與繪製作業分開,並從主要執行緒進行合成。
- 決定最佳合成 / 繪製策略。
- 評估 IntersectionObserver 幾何圖形。
- 避免處理螢幕外元素和 GPU 紋理圖塊。
- 有效且準確地讓繪圖和光柵失效。
- 在 Core Web Vitals 中評估版面配置變動和最大內容繪製。
每份網頁文件都有四個獨立的屬性樹狀結構:轉換、剪輯、效果和捲動。(*) 轉換樹狀結構代表 CSS 轉換和捲動。(捲動轉換會以 2D 轉換矩陣表示)。剪輯片段樹狀圖代表溢出剪輯片段。效果樹狀圖代表所有其他視覺效果:不透明度、濾鏡、遮罩、混合模式,以及其他類型的剪輯片段,例如 clip-path。捲動樹狀圖代表捲動相關資訊,例如捲動如何連結在一起;此樹狀圖需要在合成器執行緒上執行捲動。屬性樹狀結構中的每個節點都代表 DOM 元素套用的捲動或視覺效果。如果發生多個效果的情況,同一個元素的每個樹狀結構中可能會有多個屬性樹狀結構節點。
每個樹狀結構的拓撲就像 DOM 的稀疏表示法。舉例來說,如果有三個 DOM 元素含有溢位剪輯片段,則會有三個剪輯片段樹狀節點,而剪輯片段樹狀結構會遵循溢位剪輯片段之間的包含區塊關係。樹木之間也有連結。這些連結會指出節點的相對 DOM 階層,以及應用的順序。舉例來說,如果 DOM 元素上的轉換位於另一個含有篩選器的 DOM 元素下方,那麼轉換會在篩選器前套用。
每個 DOM 元素都有一個屬性樹狀狀態,這是一個 4 元組 (轉換、剪輯、效果、捲動),用於指出會對該元素產生效果的最近祖系剪輯、轉換和效果樹狀節點。這非常方便,因為有了這些資訊,我們就能確切知道套用至該元素的剪輯片段、轉換和特效清單,以及套用順序。這會告訴我們圖片在畫面上的位置,以及如何繪製。
範例
(資料來源)
<html>
<div style="overflow: scroll; width: 100px; height: 100px;">
<iframe style="filter: blur(3px);
transform: rotateZ(1deg);
width: 100px; height: 300px"
id="one" srcdoc="iframe one"></iframe>
</div>
<iframe style="top:200px;
transform: scale(1.1) translateX(200px)" id=two
srcdoc="iframe two"></iframe>
</html>
針對上述範例 (與前言中的範例略有不同),以下是產生的屬性樹狀結構中的主要元素:
顯示清單和繪圖區塊
顯示項目包含可透過 Skia 算繪的低階繪圖指令 (請參閱此處)。顯示項目通常很簡單,只需幾個繪圖指令,例如繪製邊框或背景。繪製樹狀結構的檢查會依照 CSS 繪製順序,對版面配置樹狀結構和相關片段進行迭代,以產生顯示項目清單。
例如:
<div id="green" style="background:green; width:80px;">
Hello world
</div>
<div id="blue" style="width:100px;
height:100px; background:blue;
position:absolute;
top:0; left:0; z-index:-1;">
</div>
這段 HTML 和 CSS 會產生下列顯示清單,其中每個單元格都是顯示項目:
檢視畫面的背景 | #blue 背景音訊 |
#green 背景音訊 |
#green 內嵌文字 |
---|---|---|---|
drawRect 大小為 800x600,顏色為白色。 |
drawRect 大小為 100x100,位於 0,0 位置,顏色為藍色。 |
drawRect 大小為 80x18,位於 8,8 位置,顏色為綠色。 |
drawTextBlob 的位移為 8,8,文字為「Hello world」。 |
顯示項目清單的排序方式為由後至前。在上述範例中,綠色 div 在 DOM 順序中位於藍色 div 之前,但 CSS 繪圖順序要求負 z-index 藍色 div 繪圖順序 (步驟 3) 應在綠色 div (步驟 4.1) 之前。顯示項目大致對應於 CSS 繪圖順序規格中的原子步驟。單一 DOM 元素可能會產生多個顯示項目,例如 #green 有背景的顯示項目,以及內嵌文字的另一個顯示項目。這個細緻度對於呈現 CSS 繪製順序規格完整的複雜度相當重要,例如由負邊界產生的交錯:
<div id="green" style="background:green; width:80px;">
Hello world
</div>
<div id="gray" style="width:35px; height:20px;
background:gray;margin-top:-10px;"></div>
這會產生下列顯示清單,其中每個單元格都是顯示項目:
檢視畫面的背景 | #green 背景音訊 |
#gray 背景音訊 |
#green 內嵌文字 |
---|---|---|---|
drawRect 大小為 800x600,顏色為白色。 |
drawRect 大小為 80x18,位於 8,8 位置,顏色為綠色。 |
drawRect 的大小為 35x20,位於 8,16 的位置,顏色為灰色。 |
drawTextBlob 的位移為 8,8,文字為「Hello world」。 |
系統會儲存顯示項目清單,並在日後更新時重複使用。如果版面配置物件在繪製樹狀檢視期間未變更,其顯示項目會從先前的清單複製。另一項最佳化功能則是依賴 CSS 繪圖順序規格說明書的屬性:堆疊內容會以原子方式繪製。如果堆疊內容中沒有任何版面配置物件發生變更,則繪製樹狀檢查會略過堆疊內容,並從先前的清單複製顯示項目的整個序列。
在繪製樹狀圖時,系統會維持目前的屬性樹狀圖狀態,並將顯示項目清單分組為共用相同屬性樹狀圖狀態的「區塊」。請參考以下範例:
<div id="scroll" style="background:pink; width:100px;
height:100px; overflow:scroll;
position:absolute; top:0; left:0;">
Hello world
<div id="orange" style="width:75px; height:200px;
background:orange; transform:rotateZ(25deg);">
I'm falling
</div>
</div>
這會產生下列顯示清單,其中每個儲存格都是顯示項目:
檢視畫面的背景 | #scroll 背景音訊 |
#scroll 內嵌文字 |
#orange 背景音訊 |
#orange 內嵌文字 |
---|---|---|---|---|
drawRect 大小為 800x600,顏色為白色。 |
drawRect 大小為 100x100,位於 0,0 位置,顏色為粉紅色。 |
drawTextBlob 的位移為 0,0,文字為「Hello world」。 |
drawRect 大小為 75x200,位於 0,0 位置,顏色為橘色。 |
drawTextBlob 的文字為「I'm falling」,位置為 0,0。 |
轉換屬性樹狀結構和繪圖區塊會變成以下內容 (為了簡潔起見,我們簡化了內容):
繪圖區塊的排序清單 (即一組顯示項目和屬性樹狀狀態) 是轉譯管線的圖層化步驟輸入內容。整個繪圖區塊清單可合併為單一疊合圖層,並一起進行算繪,但這會導致每次使用者捲動時都需要進行耗時的算繪作業。您可以為每個繪圖區塊建立經過合成的圖層,並個別將其轉為點陣圖,以避免所有重新轉為點陣圖的作業,但這會迅速耗盡 GPU 記憶體。在進行分層處理時,您必須在 GPU 記憶體與降低變更成本之間取得平衡。一般來說,建議您預設合併區塊,並且不要合併具有屬性樹狀圖狀態的繪圖區塊,因為這些屬性樹狀圖狀態會在合成器執行緒中變更,例如合成器執行緒捲動或合成器執行緒轉換動畫。
上述範例應產生兩個合成的圖層:
- 包含繪圖指令的 800x600 複合圖層:
drawRect
,大小為 800x600,顏色為白色drawRect
大小為 100x100,位於 0,0 位置,顏色為粉紅色
- 144x224 複合圖層,其中包含繪圖指令:
drawTextBlob
的位移為 0,0,文字為「Hello world」- 翻譯 0,18
rotateZ(25deg)
drawRect
大小為 75x200,位於 0,0 位置,顏色為橘色drawTextBlob
位置為 0,0,文字為「I'm falling」
如果使用者捲動 #scroll
,第二個合成的圖層會移動,但不需要經過算繪。
範例:在前一個屬性樹狀結構的部分,有六個繪圖區塊。以下是轉換、剪輯、效果和捲動的屬性樹狀狀態:
- 文件背景:文件捲動、文件剪輯、根目錄、文件捲動。
- div 的水平、垂直和捲動角落 (三個獨立的繪圖區塊):文件捲動、文件剪輯、
#one
模糊處理、文件捲動。 - Iframe
#one
:#one
旋轉、溢出捲動剪輯片段、#one
模糊處理、div 捲動。 - Iframe
#two
:#two
縮放、文件剪輯、根目錄、文件捲動。
合成器影格:介面、算繪介面和 GPU 紋理圖塊
瀏覽器和算繪程序會管理內容的點陣化,然後將合成器影格提交至 Viz 程序,以便在螢幕上呈現。合成器影格代表如何將經過掃描處理的內容拼接在一起,並使用 GPU 有效繪製。
資訊方塊
理論上,算繪程序或瀏覽器程序轉譯器可以將像素光柵化為單一紋理,並以算繪器檢視區的完整大小提交該紋理,然後將該紋理提交給 Viz。為了顯示紋理,顯示轉譯器只需將單一紋理的像素複製到影格緩衝區的適當位置 (例如螢幕)。不過,如果該合成器想更新單一像素,就必須重新將整個檢視區塊轉為點陣圖,並將新的紋理提交給 Viz。
而是將可視區域分割為圖塊。每個動態磚都會使用獨立的 GPU 紋理動態磚,以掃描的像素作為檢視區的一部分。這樣一來,轉譯器就能更新個別圖塊,甚至只變更現有圖塊在畫面上的位置。舉例來說,捲動網站時,現有圖塊的位置會向上移動,只有在某些情況下,才需要將新圖塊轉為點陣圖,以便顯示頁面底部的內容。
四邊形和介面
GPU 紋理圖塊是一種特殊的 四邊形,這只是某個紋理類別的花俏名稱。四邊形會識別輸入紋理,並指出如何轉換紋理並套用視覺效果。舉例來說,一般內容方塊會具有轉換,可指出方塊格線中的 x、y 位置。
這些經過算繪的圖塊會包裝在算繪傳遞中,而這項傳遞是四邊形的清單。算繪通道不含任何像素資訊,而是提供有關繪製每個四邊形的位置和方式的指令,以產生所需的像素輸出內容。每個 GPU 紋理圖塊都有一個繪圖四邊形。顯示合成器只需依序檢查四邊形清單,並以指定的視覺效果繪製每個四邊形,即可為算繪階段產生所需的像素輸出內容。您可以在 GPU 上有效地合成算繪通道的繪圖四邊形,因為允許的視覺效果經過精心挑選,可直接對應至 GPU 功能。
除了經過算繪的圖塊之外,還有其他類型的繪圖四邊形。舉例來說,有些純色繪圖四邊形完全沒有紋理支援,有些紋理繪圖四邊形則是用於非平鋪紋理,例如影片或畫布。
合成器影格也可能嵌入其他合成器影格。舉例來說,瀏覽器合成器會產生含有瀏覽器 UI 的合成器影格,以及用於嵌入轉譯合成器內容的空白矩形。另一個例子是網站隔離的 iframe。這項嵌入作業是透過途徑完成。
當合成器提交合成器影格時,會附帶一個稱為「表面 ID」的 ID,讓其他合成器影格透過參照嵌入。Viz 會儲存以特定表面 ID 提交的最新合成器影格。其他合成器影格稍後可透過表面繪圖四邊形參照該影格,因此 Viz 就會知道要繪製哪些內容。(請注意,表面繪圖四邊形只包含表面 ID,而非紋理)。
中繼算繪傳遞
某些視覺效果 (例如許多濾鏡或進階混合模式) 需要將兩個或更多四邊形繪製至中介紋理。接著,系統會將中間材質繪製至 GPU 上的目的地緩衝區 (或其他中間材質),同時套用視覺效果。為此,合成器影格實際上包含轉譯階段清單。總是會有一個根轉譯作業,這是最後繪製的作業,其目的地會對應至影格緩衝區,且可能還有其他作業。
由於多個算繪通道是可能的,因此才有「算繪通道」這個名稱。每個 pass 都必須在 GPU 上以多個「pass」依序執行,而單一 pass 則可在單一大量並行 GPU 運算中完成。
匯總
多個合成器影格會提交至 Viz,且需要一併繪製至螢幕。匯總階段會將這些圖層轉換為單一匯總合成器影格,藉此完成匯總作業。匯總功能會使用指定的合成器影格取代表面繪圖四邊形。這也是最佳化不必要的中介紋理或畫面外內容的機會。舉例來說,在許多情況下,網站隔離 iframe 的合成器影格不需要自己的中介紋理,而且可以透過適當的繪圖四邊形直接繪製至影格緩衝區。匯總階段會找出這類最佳化項目,並根據個別轉譯合成器無法存取的全球知識套用這些最佳化項目。
範例
以下是代表本篇文章開頭範例的轉譯器影格。
foo.com/index.html
表面:ID=0- 算繪通道 0:繪製至輸出。
- 算繪區塊繪製四邊形:以 3 像素模糊效果繪製,並裁剪至算繪區塊 0。
- 算繪通道 1:
- 為
#one
iframe 的方塊內容繪製四邊形,並為每個四邊形指定 x 和 y 位置。
- 為
- 算繪通道 1:
- 途徑繪圖四邊形:ID 為 2,使用縮放和平移轉換繪製。
- 算繪區塊繪製四邊形:以 3 像素模糊效果繪製,並裁剪至算繪區塊 0。
- 算繪通道 0:繪製至輸出。
- 瀏覽器 UI 途徑:ID=1
- 算繪通道 0:繪製至輸出。
- 為瀏覽器 UI 繪製四邊形 (也要平鋪)
- 算繪通道 0:繪製至輸出。
bar.com/index.html
surface: ID=2- 算繪通道 0:繪製至輸出。
- 為
#two
iframe 的內容繪製四邊形,並為每個四邊形指定 x 和 y 位置。
- 為
- 算繪通道 0:繪製至輸出。
插圖由 Una Kravets 繪製。