轉譯 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 的版面配置系統中有系統地修正這些問題,這也是許多工程師多年努力的成果。

在本篇文章中,我將說明這類重大架構變更如何減少及緩解各種錯誤和效能問題。

版面配置引擎架構的鳥瞰圖

先前,Blink 的版面配置樹狀結構是我們稱為「可變動樹狀結構」的東西。

顯示下文所述的樹狀結構。

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

這些物件會在渲染之間保留。樣式發生變更時,我們會將該物件標示為髒汙,並同樣標示樹狀結構中的所有父項。執行轉譯管道的版面配置階段時,我們會清理樹狀結構、檢查任何髒物物件,然後執行版面配置,讓這些物件處於清潔狀態。

我們發現這種架構會導致許多類型的問題,如下所述。不過,我們先來回顧一下版面配置的輸入和輸出內容。

在這個樹狀結構中的節點上執行版面配置,在概念上會採用「樣式加 DOM」,以及來自父項版面配置系統 (格狀、區塊或 Flex) 的任何父項限制,執行版面配置限制演算法,並產生結果。

先前所述的概念模型。

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

片段樹狀結構。

我先前介紹了不變更的 Fragment 樹狀結構,說明其如何設計,以便重複使用先前樹狀結構的大部分內容,用於逐步變更版面配置。

此外,我們也會儲存產生該片段的父項限制條件物件。我們會將此做為快取鍵使用,我們會在下文進一步說明。

我們也重新編寫內嵌 (文字) 版面配置演算法,以符合新的不可變更架構。它不僅可產生不可變動的平面清單表示法,用於內嵌式版面配置,還提供段落層級快取功能,可加快重新版面配置作業、每段落形狀,以便在元素和字詞之間套用字型功能,以及使用 ICU 的新 Unicode 雙向演算法、許多正確性修正等。

版面配置錯誤類型

大致而言,版面配置錯誤可分為四種,每種都有不同的根本原因。

正確性

當我們想到轉譯系統中的錯誤時,通常會想到正確性,例如:「瀏覽器 A 有 X 行為,而瀏覽器 B 有 Y 行為」,或是「瀏覽器 A 和 B 都出錯」。這項工作過去耗費了我們大量時間,而且過程中我們一直在與系統奮戰。常見的失敗模式是針對某個錯誤進行非常有針對性的修正,但幾週後發現我們在系統的另一個 (看似不相關) 部分造成了回歸。

先前的文章所述,這表示系統非常不穩定。具體來說,我們在任何類別之間都沒有明確的合約,導致瀏覽器工程師依賴不該依賴的狀態,或誤解系統其他部分的某些值。

舉例來說,我們曾經在一年多內,發現一連串與 Flex 版面配置相關的 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();
}

修正這類問題通常會導致嚴重的效能倒退 (請參閱下方的過度無效化),而且修正方式非常精細。

目前 (如上所述),我們有一個不可變動的父項約束條件物件,可描述從父項版面配置傳遞至子項的所有輸入內容。我們會將此資訊與產生的不可變更片段一併儲存。因此,我們會在集中位置diff這兩個輸入內容,判斷子項是否需要執行另一個版面配置階段。這個差異比較邏輯雖然複雜,但已妥善控管。針對這類未充分驗證的問題進行偵錯,通常會導致手動檢查兩個輸入內容,並決定輸入內容中哪些內容已變更,以便需要進行另一次版面配置傳遞作業。

由於建立這些獨立物件相當簡單,因此修正這個差異程式碼通常也相當簡單,而且可以輕鬆進行單元測試

比較固定寬度和百分比寬度的圖片。
固定寬度/高度元素不會在意可用大小是否增加,但以百分比為準的寬度/高度會。available-size 會顯示在 Parent Constraints 物件上,並在差異比較演算法中執行這項最佳化作業。

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

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 或 GridLayout) 會執行測量傳遞作業,以判斷每個子項的大小,然後執行版面配置傳遞作業,將所有子項拉伸至此大小。這項行為是 Flex 和 GridLayout 的預設行為。

兩組方塊,第一組顯示測量傳遞中方塊的內在大小,第二組則是所有布局的高度都相同。

這些兩次掃描版面配置在效能方面一開始是可接受的,因為使用者通常不會將這些版面配置巢狀化。不過,隨著內容變得更複雜,我們開始發現重大的效能問題。如果您未將評估階段的結果快取,版面配置樹狀結構會在「measure」狀態和最終「layout」狀態之間發生抖動。

說明文字中說明的單一、兩次和三次掃描版面配置。
在上圖中,我們有三個 <div> 元素。簡單的單次掃描版面配置 (例如區塊版面配置) 會造訪三個版面配置節點 (複雜度為 O(n))。不過,對於兩次傳送的版面配置 (例如 Flex 或 GridLayout),這可能會導致此範例的訪問複雜度為 O(2n)。
圖表顯示版面配置時間呈指數級增加。
這張圖片和示範顯示採用格線版面配置的指數版面配置。由於 Grid 已移至新架構,因此 Chrome 93 已修正此問題

先前我們會嘗試在 Flex 和 GridLayout 版面配置中加入非常明確的快取,以對抗這類成效急降的情況。這項做法有效 (我們在 Flex 上取得了重大進展),但我們一直在與超出和未達無效的錯誤奮戰。

LayoutNG 可讓我們為版面配置的輸入和輸出內容建立明確的資料結構,此外,我們也已建構測量和版面配置階段的快取。這會將複雜度降回 O(n),讓網頁開發人員可預測線性效能。如果版面配置執行三次掃描,我們也會快取該次掃描。這可能會為日後安全引入更進階的版面配置模式提供契機,這就是 RenderingNG 如何從根本上解鎖可擴充性的例子。在某些情況下,格狀版面配置可能需要三次版面配置,但目前這種情況極為罕見。

我們發現,開發人員在版面配置方面遇到效能問題時,通常是因為版面配置時間的指數型錯誤,而非管道版面配置階段的原始吞吐量。如果小幅的漸進式變更 (一個元素變更單一 CSS 屬性) 導致版面配置時間為 50 到 100 毫秒,這很可能是指數版面配置錯誤。

摘要說明

版面配置是極為複雜的領域,我們並未涵蓋所有有趣的細節,例如內嵌版面配置最佳化 (也就是整個內嵌和文字子系統的運作方式),甚至連這裡提到的概念也只是略微提及,並未深入探討。不過,我們希望您能瞭解,系統架構的改善工作若能有系統地進行,長期下來就能帶來超乎預期的成果。

不過,我們知道還有許多工作要做。 我們知道有一系列問題 (包括效能和正確性) 正在解決中,也非常期待 CSS 推出新的版面配置功能。我們認為 LayoutNG 的架構可安全且可行地解決這些問題。

Una Kravets 拍攝的一張圖片 (你知道是哪張)