這些資料結構如下:
- 影格樹狀結構由本機和遠端節點組成,代表轉譯程序中的網路文件和 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,或是將 iframe 中的部分與 DOM 中的其他元素遮蔽。
視覺化屬性更新刊登序列
裝置縮放係數和可視區域大小等視覺屬性會影響算繪的輸出內容,且必須在本機影格樹狀結構片段之間同步處理。每個本機框架樹狀結構片段的根都有一個相關聯的小工具物件。視覺屬性更新會移至主要頁框的小工具,才會傳播至由上而下其餘的小工具。
舉例來說,當可視區域大小改變時:
這項程序不會立即執行,因此複製的視覺屬性也會包含同步權杖。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)
- (方塊 <span>、1)
- (傳送文字「Hi」的 0)
- (線條方塊, 3)
- (方塊 <b>、1)
- (傳送文字「the」,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 轉換矩陣表示)。裁剪樹狀結構代表溢位片段。效果樹狀結構代表所有其他視覺效果,包括不透明度、濾鏡、遮罩、混合模式,以及剪輯路徑等其他類型的片段。捲動樹狀結構代表捲動的相關資訊,例如一起捲動鏈結的方式;必須在合成器執行緒上執行捲動作業。屬性樹狀結構中的每個節點,都代表由 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 繪製順序需要在 (步驟 3) 之前繪製綠色 div 之前的 z-index 藍色 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 位置為 0,0,且文字「I'm Falling」。 |
轉換屬性樹狀結構和繪製區塊就會 (為求簡潔而簡化):
套用序數區塊清單 (顯示項目群組及屬性樹狀結構狀態) 是對轉譯管道步驟分層的輸入內容。整份油漆區塊清單可以合併為單一合成層並光柵化,但這需要使用者每次捲動畫面時,都需耗費高成本的光柵化處理。系統可能會為每個繪製區塊建立複合層並個別光柵化,以免重新光柵化,但會快速耗盡 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 上的目的地緩衝區 (或可能是其他中繼紋理),同時套用視覺效果。為了達成這個目標,合成器框架實際上包含算繪通道清單。系統一律會提供根算繪通道,前者會繪製最後,其目的地會對應至影格緩衝區,也可能更多。
有多個算繪通道可能都會說明「算繪通道」這個名稱。每個票證都必須在 GPU 上依序執行 (分多次「傳遞」),但單一票證可以在單一大量平行 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:繪製至輸出內容。
- 繪製瀏覽器使用者介面的四邊形 (同時並排)
- 算繪傳遞 0:繪製至輸出內容。
bar.com/index.html
介面:ID=2- 算繪傳遞 0:繪製至輸出內容。
- 針對
#two
iframe 的內容繪製序列,每個物件都有 x 和 y 位置。
- 針對
- 算繪傳遞 0:繪製至輸出內容。
插圖:Una Kravets。