轉譯 NG 深入解析:LayoutNG

伊恩.基爾帕特里克 (Ian Kilpatrick)
Ian Kilpatrick
石井孝治 (Koji Ishi)
Koji Ishi

我是 Blink 配置團隊的工程主管 Ian Kilpatrick 和 Koji Ishii我任職於 Blink 團隊之前,曾是前端工程師 (在 Google 曾擔任「前端工程師」角色)、Google 文件、雲端硬碟和 Gmail 等各項新功能。 在這五年後,我特地轉任 Blink 團隊,開始有效學習 C++,並試圖擴大處理極為複雜的 Blink 程式碼集。儘管今天,我也只能理解其中一小部分。 非常感激你在這段期間給我的時間。 很多人「復原前端工程師」這讓我感到很困擾,但我之前有許多「復原前端工程師」才成為一名「瀏覽器工程師」。

我先前在 Blink 團隊中曾親自引導我。 我是前端工程師,經常遇到瀏覽器不一致、效能問題、轉譯錯誤和缺少功能的問題。LayoutNG 能夠有利於在 Blink 的版面配置系統中有系統地修正這些問題,同時也代表許多工程師在過去幾年付出的努力總結。

在這篇文章中,我會說明這類大規模的架構變更如何減少及減輕各種錯誤和效能問題。

30,000 英尺的配置引擎架構圖

Blink 的版面配置樹狀結構先前稱為「可變動樹狀結構」。

顯示樹狀結構,如下列文字所示。

版面配置樹狀結構中的每個物件都包含輸入資訊,例如父項設定的可用大小、任何浮點值的位置,以及「輸出」資訊,例如物件的最終寬度和高度或其 x 和 y 位置。

算繪之間會保留這些物件。樣式變更時,我們會將該物件標示為骯髒物件,同樣地,該物件在樹狀結構中的所有父項一樣。 執行轉譯管道的版面配置階段時,我們會清理樹狀結構,逐一測試有無髒汙的物件,然後執行版面配置,讓物件呈現乾淨的狀態。

我們發現這個架構導致的問題發生很多,以下將說明如下: 不過,先回顧一下,版面配置的輸入和輸出內容。

從概念上來說,在樹狀結構的節點上執行版面配置時採用「Style Plus DOM」,以及父項版面配置系統 (格線、區塊或彈性) 的任何父項限制,都會執行版面配置限制演算法,並產生結果。

先前說明的概念模型。

我們的新架構可將這個概念模型正式化。 我們還提供版面配置樹狀結構,但主要用來保存版面配置的輸入和輸出內容。針對輸出內容,我們會產生稱為「片段樹狀結構」的全新「不可變」物件。

片段樹狀結構。

我曾介紹過先前不可變更的片段樹狀結構,說明這個樹狀結構的設計方式,是如何重複使用先前樹狀結構中的大部分進行漸進式版面配置。

此外,我們會儲存產生該片段的父項限制物件。我們會把它做為「快取金鑰」,我們會在下文中說明。

此外,內嵌 (文字) 版面配置演算法也會重新編寫,以符合新的不可變架構。 這不僅為內嵌版面配置產生「不可變更的平面清單表示法」,還具有段落層級快取功能,可加快重新版面配置的速度;調整每個段落的形狀,為元素和字詞套用字型功能、使用 ICU 新增的 Unicode 雙向演算法、許多正確性修正等等。

版面配置錯誤類型

版面配置錯誤大致可分為四類 每個類別都有不同的根本原因

正確性

當我們思考轉譯系統的錯誤時,通常會考慮正確性,例如:「瀏覽器 A 有 X 的行為,而瀏覽器 B 有 Y 行為」,或是「瀏覽器 A 和 B 都無法正常運作」。 這是我們花費大量時間花費的心力 在過程中,我們不斷與系統打擊。 常見的失敗模式是針對某個錯誤套用非常針對性的修正,但後來幾週發現,該錯誤在系統的另一個部分 (看似不相關) 中發生迴歸問題。

先前的文章所述,這是相當脆弱的徵兆。以版面配置來說,我們並沒有類別之間的明確合約,導致瀏覽器工程師依賴應不需要的狀態,或誤解系統其他部分的值。

例如,我們在一年多內建構了約 10 個與彈性版面配置的錯誤鏈結。每項修正都造成系統的正確性或效能問題,另外也造成其他錯誤。

現在 LayoutNG 已明確定義了版面配置系統中所有元件之間的合約,進而發現我們可以更放心地套用變更。另外,出色的 Web Platform Tests (WPT) 專案也帶來許多好處,這項專案可讓多方為通用網路測試套件做出貢獻。

我們今天發現,如果在穩定版本上發布真實迴歸,通常它在 WPT 存放區中沒有相關測試,而且不會因為對元件合約誤解而產生任何相關測試。此外,根據錯誤修正政策,我們一律會新增 WPT 測試,確保瀏覽器不會再次出現相同的錯誤。

無效

如果您曾遇到神秘錯誤,導致您重新調整瀏覽器視窗大小或切換 CSS 屬性,會導致錯誤消失,所以您遇到不足的問題。實際上,可變動樹狀結構的一部分會被視為乾淨,但由於父項限制的某些變更,無法代表正確的輸出內容。

這在以下所述的雙重通行程序 (兩次步行版面配置樹狀結構兩次以判斷最終版面配置狀態) 版面配置模式中非常常見。舊版程式碼看起來會像這樣:

if (/* some very complicated statement */) {
  child->ForceLayout();
}

這類錯誤的修正通常是:

if (/* some very complicated statement */ ||
    /* another very complicated statement */) {
  child->ForceLayout();
}

修正這類問題通常會導致嚴重的效能迴歸問題 (請參閱下方說明是否無效),且更正內容相當詳盡。

今天 (如上所述) 我們有一個不可變更的父項限制物件,描述從父項版面配置到子項的所有輸入內容。我們會使用產生的不可變動片段進行儲存。 因此,我們會將這兩個輸入內容差異集中在一處,以便判斷子項是否需要執行另一個版面配置傳遞。這個差異比較邏輯很複雜,但相當完善。對這類無效問題進行偵錯時,通常會導致手動檢查兩個輸入內容,並決定輸入中的哪些內容會改變,進而需要另一項版面配置傳遞。

由於建立這些獨立物件很簡單,此差異比較程式碼的修正流程通常簡單又方便單元測試

比較固定寬度與百分比寬度的圖片。
固定寬度/高度元素並不在意它的可用大小增加,但寬度/高度會增加。「available-size」會顯示在「父項限制」物件中,因此做為差異演算法的一部分,會執行這項最佳化作業。

上述範例的差異程式碼如下:

if (width.IsPercent()) {
  if (old_constraints.WidthPercentageSize() 
    != new_constraints.WidthPercentageSize())
   return kNeedsLayout;
}
if (height.IsPercent()) {
  if (old_constraints.HeightPercentageSize() 
    != new_constraints.HeightPercentageSize())
   return kNeedsLayout;
}

衛生

這類錯誤類似無效情況。基本上,在先前的系統中,很難確保版面配置具有冪等性,也就是使用相同的輸入重新執行版面配置,從而產生相同的輸出內容。

在以下範例中,我們只是在兩個值之間來回切換 CSS 屬性。 但這就會形成「無限成長」的矩形。

這部影片和示範示範的是 Chrome 92 以下版本的發生原因錯誤。已在 Chrome 第 93 版中修正。

在我們先前的可變動樹狀結構中 開發出這類錯誤非常容易如果程式碼在錯誤的時間或階段中誤解物件大小或位置 (例如我們沒有「清除」先前的大小或位置),我們就會立即新增輕微的假設錯誤。這些錯誤通常不會在測試中出現,因為多數測試都聚焦於單一版面配置及轉譯。更令人擔憂的是,我們知道這些假設是必要的,某些版面配置模式才能正常運作。我們在執行最佳化作業是為了移除版面配置傳遞作業,但導入「錯誤」,這是因為版面配置模式需要傳遞兩次才能取得正確的輸出內容。

代表上述文字所描述問題的樹狀結構。
視先前的版面配置結果資訊而定,會產生非冪等的版面配置

透過 LayoutNG,由於我們有明確的輸入和輸出資料結構,但無法存取先前的狀態,因此我們已廣泛緩解這類來自版面配置系統的錯誤。

過度失效和成效

這與無效錯誤類別直接相反。通常在修正無效錯誤時,會觸發效能驟降的情況。

我們通常必須做出艱難的決定,讓自己看得更清楚,效果更勝以往。 在下一節中,我們會更深入探討我們如何減少這類效能問題。

雙通道版面配置和效能驟降

Flex 與格狀版面配置代表網路版面配置的生動程度轉變。不過,這些演算法在本質上與之前的區塊版面配置演算法截然不同。

區塊版面配置 (在幾乎所有情況下) 只需要引擎針對其所有子項執行一次版面配置。 這可以帶來良好效能,但實際表現最終卻沒有如網頁程式開發人員的期望。

舉例來說,通常您會希望所有子項的尺寸最大化。為支援此功能,上層版面配置 (Flex 或格線) 會執行測量傳遞,判斷每個子項的大小,然後版面配置傳遞,以將所有子項延展至此大小。這個行為是 Flex 和格線版面配置的預設行為。

兩組方塊在測量過程中顯示方塊的內建函式大小,第二組則版面配置的高度相同。

這種雙傳遞版面配置一開始是可接受的 因為使用者通常沒有深度嵌入版面配置然而,隨著越來越多的複雜內容出現,我們已開始發生重大的效能問題。 如果您未快取測量階段的結果,版面配置樹狀結構會在 measure 狀態和最終版面配置狀態之間進行輾轉。

說明版面配置中有一、二和三度的版面配置。
在上圖中,有三個 <div> 元素。 簡單的單向版面配置 (例如區塊版面配置) 會造訪三個版面配置節點 (複雜 O(n))。然而,針對雙通道的版面配置 (例如 Flex 或格線),本範例可能會產生 O(2n) 造訪的複雜度。
顯示版面配置時間指數增加的圖表。
這張圖片和示範圖片顯示採用格線版面配置的指數版面配置。在 Chrome 93 版中,這是因將網格移至新架構而修正的問題

先前我們會嘗試在彈性和格線版面配置中加入非常具體的快取,藉此打擊這種效能波動。雖然這種做法有效 (和我們與 Flex 的合作很遠),但一直在對抗嚴重和無效的錯誤。

LayoutNG 可讓我們針對版面配置的輸入和輸出內容建立明確的資料結構,並在上方建構測量和版面配置傳遞的快取。這會讓複雜程度回到 O(n),進而為網頁程式開發人員帶來可預測的線性效能。如果有版面配置進行三度傳遞的版面配置,我們也會快取這些經過快取。 這也許可以開創機會,以安全的方式推出更進階的版面配置模式。請參閱 RenderNG 的基本原則,瞭解如何提升遊戲的擴充性。在某些情況下,格狀版面配置可能需要三向版面配置,但目前的情況很少見。

我們發現,當開發人員遇到版面配置特有的效能問題時,通常是因為指數版面配置時間錯誤,而非管道版面配置階段的原始處理量。如果小幅漸進式變更 (一個元素變更單一 CSS 屬性) 造成 50 到 100 毫秒的版面配置,就可能是指數版面配置錯誤。

摘要說明

版面配置是非常複雜的領域,我們沒有涵蓋各種有趣的細節,例如內嵌版面配置最佳化 (實際上整個內嵌版面配置和文字子系統的運作方式),甚至這裡介紹的概念也只涵蓋表面上的概念,細節也稍有浮現。 然而,希望我們已經見過系統如何有系統地改善系統架構,長期下來就能帶來超乎可觀的收益。

不過,我們深知還有許多進步空間。 我們瞭解目前的問題類別 (包括效能和正確性),並且對 CSS 即將推出的新版面配置功能感到期待。 我們認為 LayoutNG 架構有助於安全解決這些問題。

一張圖片 (你認得的是 Una Kravets)