轉譯 NG 深入解析:LayoutNG

Ian Kilpatrick
Ian Kilpatrick
Koji Ishi
Koji Ishi

我是 Ian Kilpatrick 是 Blink 配置團隊的工程主管和井井澤西 (Koji Ishii)。 加入 Blink 團隊前 在 Google 擔任「前端工程師」之前,我擔任前端工程師 Google 文件、雲端硬碟和 Gmail 等多項新功能, 擔任該職位大約五年後,我轉變成 Blink 團隊 有效率地學習 C++ 工作時 嘗試逐步增加複雜的 Blink 程式碼集 就算我今天只瞭解其中的一小部分。 非常感謝在這段期間與我討論的意義。 其實有許多「修復前端工程師」這件事讓我備感壓力轉換為「瀏覽器工程師」。

從過往的經驗來看,我決定在 Blink 團隊中親自體驗。 身為前端工程師,我不斷遇到瀏覽器不一致的問題 效能問題、轉譯錯誤和缺少功能 LayoutNG 讓我們有機會在 Blink 的版面配置系統中系統性地修正問題。 代表許多工程師。

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

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

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

顯示以下文字說明的樹狀結構。

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

這些物件會在兩次算繪之間保留。 當樣式變更發生時 我們將該物件標示為骯髒,而且樹狀結構中的所有父項都相同。 執行轉譯管道的版面配置階段時 接著,我們會清理樹狀結構、探索任何骯髒的物件,然後執行版面配置,讓物件變成乾淨的狀態。

我們發現這個架構會造成許多類別的問題 這部分會在下方說明 不過,我們先讓我們回顧一下版面配置的輸入和輸出內容。

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

先前介紹的概念模型。

我們的新架構會標準化這個概念模型。 我們仍會提供版面配置樹狀結構,但主要用於保留版面配置的輸入和輸出內容。 針對輸出,我們會產生一個全新的immutable物件,稱為「片段樹狀結構」immutable

片段樹狀結構。

我介紹了 先前不可變動的片段樹狀結構 描述這項設計如何重複使用前一個樹狀結構的大部分內容,做為漸進式的版面配置。

此外,我們也會儲存產生片段的父項限制條件物件。 我們會使用這個金鑰做為快取金鑰,我們會在下方詳細說明。

此外,系統也會重新編寫內嵌 (文字) 版面配置演算法,以符合新的不可變架構。 它不僅會產生 不可變動的平面清單表示法 這項功能還具備段落層級快取功能,可加快重新配置速度 每個段落的形狀都套用字型功能 新的萬國碼 (Unicode) 雙向演算法,使用 ICU 修正許多正確性,還有更多的地方。

版面配置錯誤類型

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

正確性

思考轉譯系統中的錯誤時,通常會想到正確性 例如:「瀏覽器 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 或格線) 將會執行測量過程 來判定每個子項的大小 那麼版面配置傳遞會將所有子項延展至此大小。 這是彈性和格線版面配置的預設行為。

有兩組方塊,第一組在測量過程中顯示了方塊的內建大小,第二組則呈現相同高度。

這些雙傳遞版面配置最初符合效能, 因為使用者通常沒辦法深入巢狀 然而,隨著內容日益複雜,我們陸續發現出現重大效能問題。 如果未快取測量階段的結果 版面配置樹狀結構會在其「測量」狀態和最終版面配置狀態之間衝突。

說明文字中說明的一、二和三道式版面配置。
上圖有三個 <div> 元素, 簡單的單通道版面配置 (如同區塊版面配置) 會造訪三個版面配置節點 (複雜 O(n))。 不過,如果是雙道版面配置 (例如彈性或格線), 這可能會導致本例中的 O(2n) 造訪較為複雜。
顯示版面配置時間指數增加的圖表。
這張圖片和示範顯示採用格線版面配置的指數版面配置。在 Chrome 93 版中修正了這個問題,原因是將 Grid 移至新架構

先前我們會嘗試在彈性和格線版面配置中新增非常特定的快取,以解決這類危機。 這個方法有效 (而且我們與 Flex 遙遙領先), 卻也持續與嚴重的無效錯誤打擊

LayoutNG 讓我們能建立明確的資料結構,用於版面配置的輸入和輸出、 而且,我們還建構了測量和版面配置傳遞的快取。 這會讓複雜度回到 O(n) 為網頁程式開發人員帶來可預測的線性成效 在某些情況下,如果版面配置會執行三段式版面配置,我們也會直接快取傳遞。 這可能會讓人有機會在日後的一個例子中,安全地導入更進階的版面配置模式 提升整體擴充性。 在某些情況下,格線版面配置可能需要使用三條版面配置,但目前極少發生。

我們發現,如果開發人員遇到有關版面配置、版面配置的問題 這通常是指數式的版面配置時間錯誤所致,而非管道版面配置階段的原始處理量。 如果稍微漸進式變更 (一個元素變更一個單一 CSS 屬性) 會導致版面配置變成 50-100 毫秒, 這可能是指數版面配置錯誤

摘要說明

版面配置是極為複雜的區域 也沒有討論到內嵌版面配置最佳化等各種有趣的細節 (實際上是內嵌和文字子系統的運作方式) 即使這裡介紹的概念 都只粗估了表面 而且雜亂無章的細節 然而,希望我們已經說明瞭如何有系統地改善系統架構,長期下來能帶來可觀的收益。

儘管如此,我們早已知道還有很多工作要做。 我們知道目前正努力解決的問題類別 (效能和正確性) 也很期待 CSS 即將推出的新版面配置功能 我們相信 LayoutNG 的架構可讓解決這些問題的安全且易於操作。

一張圖像 (你知道哪張!) 由 Una Kravets 製作