RenderingNG の詳細: LayoutNG

Ian Kilpatrick
Ian Kilpatrick
Koji Ishi
Koji Ishi

私は Ian Kilpatrick です Blink レイアウト チームのエンジニアリング リードです。 Blink チームで働く前に 私はフロントエンド エンジニアでした(Google に「フロントエンド エンジニア」の役割が生まれる前)。 Google ドキュメント、ドライブ、Gmail 内の機能を構築しています。 その職務に就いて約 5 年経ち、大金を賭けて Blink チームに ジョブで効果的に C++ を学習する 非常に複雑な Blink コードベースでの 強化に取り組みました 現在でも、その内容のごく一部しか理解できていません。 この期間に与えられた時間に感謝しています。 「フロントエンド エンジニアを回復させる」ことが多く行われていることに安心しました。「ブラウザ エンジニア」へと移行です。

Blink チームに所属していた過去の経験から、個人的に助言を受けています。 私はフロントエンドエンジニアとしてブラウザの不整合に 頻繁に直面していました レンダリングのバグ、機能の欠落などです LayoutNG は、Blink のレイアウト システム内の問題を体系的に修正するのを助ける機会となりました。 多くのエンジニアが開発したプロダクトと取り組みを続けています。

この投稿では、このような大規模なアーキテクチャの変更によって、さまざまな種類のバグやパフォーマンスの問題がどのように軽減または軽減されるかを説明します。

レイアウト エンジン アーキテクチャの概要

以前は、Blink のレイアウト ツリーを「可変ツリー」と呼んでいました。

次のテキストで説明されているとおりにツリーを表示します。

レイアウト ツリーの各オブジェクトには入力情報が含まれていました。 たとえば親から指定されたサイズ、 浮動小数点数の位置、出力情報 (オブジェクトの最終的な幅と高さ、x 位置と y 位置など)を設定します。

これらのオブジェクトはレンダリングとレンダリングの間に保持されています。 スタイルが変更されると そのオブジェクトをダーティとしてマークし 同様にツリー内のすべての親としてマークしました レンダリング パイプラインのレイアウト フェーズを実行したとき、 次に、ツリーをクリーニングし、汚れたオブジェクトをすべて検査してから、レイアウトを実行してクリーンな状態にします。

このアーキテクチャはさまざまな問題を引き起こし、 以下で説明します しかし、まずは一歩引いて、レイアウトの入力と出力について考えてみましょう。

このツリーのノードでレイアウトを実行すると、概念的には「スタイルと DOM」が および親レイアウト システム(グリッド、ブロック、Flex)の親制約 レイアウト制約アルゴリズムを実行し、結果を生成します。

前述のコンセプト モデル。

Google の新しいアーキテクチャは、この概念モデルを形式化したものです。 レイアウト ツリーも残っていますが、主にレイアウトの入力と出力を保持するために使用します。 出力については、フラグメント ツリーと呼ばれる、まったく新しい不変オブジェクトを生成します。

フラグメント ツリー。

ここでは、 以前のイミュータブル フラグメント ツリー 前のツリーの大部分を増分レイアウトで再利用する仕組みについて説明しています。

さらに、そのフラグメントを生成した親制約オブジェクトを保存します。 これをキャッシュキーとして使用します(詳細は下記を参照)。

インライン(テキスト)レイアウト アルゴリズムも、新しいイミュータブル アーキテクチャに合わせて書き換えられています。 Kubernetes では、 不変のフラットリスト表現 インライン レイアウトに対応しており、再レイアウトを高速化するための段落レベルのキャッシュ機能も備えています。 要素や単語全体にフォント機能を適用する ICU を使用した新しい Unicode 双方向アルゴリズム、多数の正確性の修正など。

レイアウトのバグの種類

レイアウトのバグは、大きく分けて 4 つのカテゴリに分類されます。 それぞれ根本原因が 異なります

正確性

レンダリング システムのバグとは通常、正確性、 例: 「ブラウザ A の動作は X であるが、ブラウザ B の動作は Y である」 「ブラウザ A と B の両方が破損しています」と表示されることもあります。 以前はこの作業に多くの時間を費やしていましたが、 その過程で、絶えずシステムとの闘いを繰り広げていました。 よくある失敗は、1 つのバグに的を絞った修正を適用することで、 数週間後、システムの別の(一見無関係に見える)部分で回帰を引き起こしたことが判明しました。

以前の投稿で説明されているように、 これはシステムが非常に脆弱であることを示しています。 特にレイアウトについては、クラス間にクリーンなコントラクトがなく、 ブラウザ エンジニアが依存すべきでない状態、 システムの別の部分の値を誤って解釈したりする可能性があります。

一例として、ある時点で 1 年以上の期間で 10 件ほどの Flex レイアウトに関係します 修正のたびに、システムの一部に正確性またはパフォーマンスの問題が生じました。 別のバグにつながります

LayoutNG でレイアウト システム内のすべてのコンポーネント間のコントラクトが明確に定義されたので、 より自信を持って変更を適用できることがわかりました。 また、Google は、優れた Web Platform Tests(WPT)プロジェクトから大きな恩恵を受けています。 複数のパーティが共通のウェブテスト スイートに貢献できます。

今 Stable チャンネルで真の回帰をリリースすると、 通常、WPT リポジトリには関連するテストがありません。 コンポーネント契約の誤解が原因ではないからです。 また、バグの修正ポリシーの一環として、新しい WPT テストを必ず追加しています。 ブラウザで同じミスが起こらないようにします

無効化が不十分

ブラウザ ウィンドウのサイズ変更や CSS プロパティの切り替えによって魔法のようにバグが解消されるという謎のバグが発生した場合、 無効化の問題に陥ったとします 実質的に可変ツリーの一部はクリーンと見なされ 親の制約がいくらか変わったため、正しい出力を表していませんでした。

これは、2 段階認証モデルと 以下で説明するレイアウト モード(最終的なレイアウト状態を決定するためにレイアウト ツリーを 2 回ウォークする)というレイアウト モードを使用します。 以前のコードは次のようになります。

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

通常、このタイプのバグの修正は次のように行います。

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

このタイプの問題を修正すると、通常はパフォーマンスが大幅に低下します。 (下記の「過度な無効化」を参照)が、修正するには非常に繊細でした。

現在(前述)、親レイアウトから子へのすべての入力を記述する不変の親制約オブジェクトがあります。 これを結果として得られる不変のフラグメントとともに保存します。 そのため これら 2 つの入力を差分で参照して、子が別のレイアウトパスを実行する必要があるかどうかを判断するための一元化された場所があります。 この差分判定ロジックは複雑ですが、十分に包含されています。 このような無効化に欠ける問題をデバッグすると、通常は 2 つの入力を手動で検査することになります。 入力の内容を変更し、別のレイアウトパスが必要になるようにします。

この差分コードの修正は通常簡単ですが、 独立したオブジェクトを簡単に作成できるため、簡単に単体テスト可能です。

<ph type="x-smartling-placeholder">
</ph> 固定幅の画像とパーセント幅の画像の比較 <ph type="x-smartling-placeholder">
</ph> 固定の幅と高さの要素では、指定された利用可能なサイズが増加しても関係ありませんが、割合ベースの幅と高さは関係します。使用可能なサイズavailable-sizeは親制約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;
}

ヒステリシス

この種類のバグは無効化の不足に似ています。 基本的に、以前のシステムでは、レイアウトがべき等であることを確認することは非常に困難でした。つまり、 同じ入力でレイアウトを再実行すると、同じ出力になります。

以下の例では、単純に 2 つの値の間で CSS プロパティを前後に切り替えています。 しかしそのために「無限に成長」するクリックします。

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">
</ph> 動画とデモでは、Chrome 92 以前のヒステリシスのバグを確認できます。これは Chrome 93 で修正されています。

前の可変ツリーでは このようなバグは驚くほど簡単に発生しました。 コードが誤った時間またはステージでオブジェクトのサイズや位置を読み取っていなかった場合 (たとえば、前のサイズや位置を「クリア」していなかったため)。 すぐに微妙なヒステリシスバグが追加されます テストの大部分は 1 つのレイアウトとレンダリングに重点を置いているため、通常、これらのバグはテストでは発生しません。 さらに厄介なことに、一部のレイアウト モードを正しく機能させるには、このヒステリシスが必要になることもわかっていました。 最適化によってレイアウトパスを削除するバグがありました。 「バグ」がレイアウト モードで正しい出力を得るには 2 つのパスが必要でした。

<ph type="x-smartling-placeholder">
</ph> 前のテキストで説明した問題を示すツリー。 <ph type="x-smartling-placeholder">
</ph> 以前のレイアウト結果情報によっては、レイアウトがべき等でなくなる
をご覧ください。

LayoutNG では明示的な入力および出力データ構造があるため、 以前の状態へのアクセスは許可されていないため、レイアウト システムからこの種のバグを幅広く緩和しました。

過剰な無効化とパフォーマンス

これは、無効化が過小評価されるバグの正反対のクラスです。 無効化が不十分なバグを修正すると、パフォーマンスの低下を招くことがよくあります。

多くの場合、パフォーマンスよりも正確性を優先する難しい選択を迫られました。 次のセクションでは、このようなパフォーマンスの問題をどのように緩和したのかについて詳しく説明します。

2 パス構成のレイアウトとパフォーマンスの壁の高まり

Flex レイアウトとグリッド レイアウトにより、ウェブ上のレイアウトの表現力は変化しました。 しかし、これらのアルゴリズムは、これまでのブロック レイアウト アルゴリズムとは根本的に異なっていました。

ほとんどの場合、ブロック レイアウトでは、エンジンがそのすべての子に対して 1 回だけレイアウトを実行する必要があります。 これはパフォーマンスの向上には有効ですが、ウェブ デベロッパーが望むほど表現力がなくなります。

たとえば すべての子のサイズを最大のサイズに拡張する必要があることがよくあります。 これをサポートするため、親レイアウト(Flex または Grid)は 測定パスを実行してそれぞれの子のサイズを判断し、 すべての子をこのサイズに拡張するレイアウトパスです。 この動作は、Flex レイアウトとグリッド レイアウトの両方でデフォルトになっています。

2 組のボックス。最初のボックスは測定パスのボックスの本質的なサイズを示し、2 番目のボックスはすべて同じ高さのレイアウトです。

この 2 パスレイアウトは、当初はパフォーマンスの面で許容可能でしたが、 あまりネストしていないからです しかし、より複雑なコンテンツが登場するにつれ、重大なパフォーマンスの問題が出現し始めました。 測定フェーズの結果をキャッシュに保存しないと レイアウト ツリーは、測定状態と最終的なレイアウト状態の間でスラッシングされます。

<ph type="x-smartling-placeholder">
</ph> キャプションで説明されている 1、2、3 パスのレイアウト。 <ph type="x-smartling-placeholder">
</ph> 上の画像では、3 つの <div> 要素があります。 単純なワンパス レイアウト(ブロック レイアウトなど)は、3 つのレイアウト ノード(複雑さ O(n))にアクセスします。 ただし、2 パス レイアウト(Flex や Grid など)では、 このため、この例では O(2n) のアクセスが複雑になる可能性があります。
で確認できます。
<ph type="x-smartling-placeholder">
</ph> レイアウト時間の指数関数的な増加を示すグラフ。 <ph type="x-smartling-placeholder">
</ph> この画像とデモは、グリッド レイアウトを使用した指数レイアウトを示しています。この問題は、Grid を新しいアーキテクチャに移行したため、Chrome 93 で修正されています。
で確認できます。

以前は、この種のパフォーマンスの低下に対抗するために、Flex および Grid レイアウトに非常に限定されたキャッシュを追加しようとしていました。 これはうまくいきました(Flex ではかなりうまくいきました) 無効化のバグとの戦いが続いていました。

LayoutNG を使用すると、レイアウトの入力と出力の両方に明示的なデータ構造を作成できます。 その上に測定パスとレイアウトパスのキャッシュが 構築されています これにより複雑さが O(n) に戻り、 ウェブ デベロッパーのパフォーマンスに予測的に線形性が伴います。 レイアウトが 3 パス レイアウトを行っている場合は、そのパスもキャッシュします。 これにより、将来的には、より高度なレイアウト モードを安全に導入する機会が生まれる可能性があります。 全体的な拡張性が実現します。 グリッド レイアウトで 3 パス レイアウトが必要になる場合もありますが、現時点ではごくまれです。

特にレイアウトでパフォーマンスの問題が発生すると 通常は、パイプラインのレイアウト ステージの純粋なスループットではなく、レイアウト時間の指数関数的なバグが原因です。 小さな増分変更(1 つの要素で単一の CSS プロパティを変更する)によって、レイアウトが 50 ~ 100 ミリ秒になる場合は、 指数レイアウトのバグである可能性があります

まとめ

レイアウトは非常に複雑な領域であり インラインレイアウトの最適化や (インラインとテキスト サブシステム全体が実際にどのように機能するか)、 ここでお話ししたコンセプトでさえも、ほんの一例に過ぎません。 詳しく説明しています。 しかし、システムのアーキテクチャを体系的に改善することで、長期的には大がかりな利益が得られることがわかっていれば幸いです。

とはいえ、私たちにはまだ多くの課題が残されていることは承知しています。 Google は、解決に向けて取り組んでいる種類の問題(パフォーマンスと正確性の両方)を認識しており、 CSS に導入される新しいレイアウト機能に期待しています。 LayoutNG のアーキテクチャによって、こうした問題を安全かつ簡単に解決できると Google は考えています。

Una Kravets による画像です