RenderingNG の詳細: LayoutNG ブロックの断片化

Morten Stenshorne
Morten Stenshorne

ブロックの断片化は、CSS ブロックレベルのボックス(セクションや段落など)が 1 つのフラグメント コンテナ(fragmentainer)に全体として収まらない場合に、そのボックスを複数のフラグメントに分割します。フラグメンタイナーは要素ではなく、複数列レイアウトの列またはページド メディアのページを表します。

断片化を行うには、コンテンツが断片化コンテキスト内にある必要があります。断片化のコンテキストは、通常、複数列のコンテナ(コンテンツが列に分割される)または印刷(コンテンツがページに分割される)によって確立されます。行数の多い長い段落は、最初の行を最初のフラグメントに配置し、残りの行を後続のフラグメントに配置するように、複数のフラグメントに分割する必要があります。

1 つの段落のテキストが 2 つの列に分割されている。
この例では、マルチカラム レイアウトを使用して段落が 2 つの列に分割されています。各列はフラグメンタライザーであり、フラグメント化されたフローのフラグメントを表します。

ブロックの断片化は、よく知られている別の断片化タイプである行の断片化(「行の分割」とも呼ばれます)に似ています。複数の単語で構成され、改行が許可されるインライン要素(任意のテキストノード、任意の <a> 要素など)は、複数のフラグメントに分割できます。各フラグメントは別の行ボックスに配置されます。行ボックスは、列とページのフラグメンタイザーと同等のインライン フラグメンテーションです。

LayoutNG ブロックの断片化

LayoutNGBlockFragmentation は、LayoutNG のフラグメンテーション エンジンの書き換えであり、最初は Chrome 102 でリリースされました。データ構造については、NG 以前の複数のデータ構造を、フラグメント ツリー内で直接表現される NG フラグメントに置き換えました。

たとえば、CSS プロパティ「break-before」と「break-after」の「avoid」値がサポートされるようになりました。これにより、ヘッダーの直後に改行しないようにすることができます。ページの最後がヘッダーで、そのセクションのコンテンツが次のページから始まっている場合、見た目が悪くなることがよくあります。ヘッダーのに改行することをおすすめします。

見出しの配置の例。
図 1. 最初の例では、ページの下部にヘッダーが表示されています。2 番目の例では、ヘッダーが次のページの上部に表示され、関連するコンテンツが表示されています。

Chrome は断片化オーバーフローもサポートしているため、モノリシックな(分割できないはずの)コンテンツが複数の列にスライスされず、シャドウや変換などのペイント エフェクトが正しく適用されます。

LayoutNG でのブロックの断片化が完了

コア フラグメンテーション(行レイアウト、フロート、フロー外配置などのブロック コンテナ)は Chrome 102 でリリースされました。フレックスとグリッドのフラグメンテーションは Chrome 103 でリリースされ、テーブルのフラグメンテーションは Chrome 106 でリリースされました。最後に、Chrome 108 で印刷がリリースされました。ブロックの断片化は、レイアウトの実行にレガシー エンジンに依存する最後の機能でした。

Chrome 108 以降、以前のエンジンはレイアウトの実行に使用されなくなります。

また、LayoutNG データ構造はペイントとヒットテストをサポートしていますが、レイアウト情報を読み取る JavaScript API では、offsetLeftoffsetTop などの従来のデータ構造に依存しています。

すべてを NG でレイアウトすることで、CSS コンテナクエリ、アンカーの位置指定、MathMLカスタム レイアウト(Houdini)など、LayoutNG の実装のみを持つ(以前のエンジンに対応するものがない)新機能を実装してリリースできるようになります。コンテナ クエリについては、印刷がまだサポートされていないことをデベロッパーに警告しつつ、少し前にリリースしました。

LayoutNG の最初の部分は 2019 年にリリースされました。これには、通常のブロック コンテナ レイアウト、インライン レイアウト、フロート、アウトオブフロー ポジショニングが含まれていますが、フレックス、グリッド、テーブルはサポートされていません。また、ブロックの断片化はまったくサポートされていません。フレックス、グリッド、テーブルなど、ブロックの断片化に関連するものには、従来のレイアウト エンジンを使用します。これは、断片化されたコンテンツ内のブロック要素、インライン要素、フローティング要素、アウトオブフロー要素にも当てはまります。このように複雑なレイアウト エンジンをインプレースでアップグレードするのは、非常にデリケートな作業です。

また、2019 年半ばまでに、LayoutNG ブロックの断片化レイアウトのコア機能の大部分が(フラグで)実装されています。発送に時間がかかったのはなぜですか?端的に答えると、断片化はシステムのさまざまなレガシー部分と正しく共存する必要があります。これらのレガシー部分は、すべての依存関係がアップグレードされるまで削除またはアップグレードできません。

以前のエンジンとのやり取り

レガシー データ構造は、レイアウト情報を読み取る JavaScript API を引き続き担当しているため、レガシー エンジンが理解できる方法でデータを書き戻す必要があります。これには、LayoutMultiColumnFlowThread などの従来のマルチカラム データ構造を正しく更新することも含まれます。

以前のエンジンのフォールバック検出と処理

LayoutNG ブロックの断片化で処理できないコンテンツが内部にあった場合、従来のレイアウト エンジンにフォールバックする必要がありました。コア LayoutNG の出荷時点では、Flex、Grid、Tables など、出力されたあらゆるものが含まれていた、ブロックの断片化が発生していました。これは特に難しい作業でした。レイアウト ツリー内のオブジェクトを作成する前に、以前の代替手段の必要性を検出する必要があったためです。たとえば、マルチ列コンテナの祖先があるかどうか、どの DOM ノードがフォーマット コンテキストになるかどうかを判断する前に検出する必要がありました。これは、完全な解決策がないニワトリと卵の問題ですが、誤動作が誤検出(実際には必要がないときに従来版にフォールバックする)のみである限り、問題ありません。そのレイアウト動作のバグは、Chromium にすでに存在するものであり、新しいものではありません。

プリペイント ツリー ウォーク

ペイント前は、レイアウト後でペイント前に行います。主な課題は、引き続きレイアウト オブジェクト ツリーをウォークする必要があることですが、今回は NG フラグメントになりました。では、どのように対処すればよいでしょうか。レイアウト オブジェクトと NG フラグメント ツリーの両方を同時に走査します。2 つのツリー間のマッピングは簡単ではないため、これは非常に複雑です。

レイアウト オブジェクトのツリー構造は DOM ツリーとよく似ていますが、フラグメント ツリーはレイアウトの入力ではなく、レイアウトの出力です。フラグメント ツリーは、インライン フラグメンテーション(行フラグメント)やブロック フラグメンテーション(列またはページ フラグメント)などのフラグメンテーションの効果を実際に反映するだけでなく、包含ブロックと、そのフラグメントを包含ブロックとする DOM 子孫の間に直接の親子関係も持ちます。たとえば、フラグメント ツリーでは、フロー外に配置された子孫とそれを含むブロックの間の系図チェーンに他のノードがあっても、絶対位置にある要素によって生成されたフラグメントは、それを含むブロック フラグメントの直接の子になります。

断片化の内部にフロー外に配置された要素があると、さらに複雑になる可能性があります。フロー外フラグメントは、そのフラグメントは Fragmentainer の直接の子になります(CSS が包含ブロックだと考えている子の子ではありません)。これは、以前のエンジンと共存するために解決しなければならなかった問題でした。LayoutNG は、最新のすべてのレイアウト モードを柔軟にサポートするように設計されているため、将来的にはこのコードを簡素化できるはずです。

従来のフラグメンテーション エンジンの問題

ウェブの初期に設計されたレガシー エンジンには、(印刷をサポートするために)技術的には存在していたとしても、断片化の概念が実際にはありません。断片化のサポートは、上からボルト止めされたもの(印刷)または後付けされたもの(複数列)でした。

断片化可能なコンテンツをレイアウトする場合、以前のエンジンでは、すべてを縦長のストリップとしてレイアウトしていました。幅は列またはページのインライン サイズで、高さはコンテンツを格納するために必要な高さです。この縦長のストリップはページにはレンダリングされません。仮想ページにレンダリングされた後、最終的な表示のために再配置されるようなものです。紙の新聞記事を 1 つの列に印刷し、次にハサミで複数にカットするのと概念的には同じです。(昔、一部の新聞では実際にこのような手法が使われていました)。

以前のエンジンは、ストリップ内の架空のページまたは列の境界を追跡します。これにより、境界を越えて収まらないコンテンツを次のページまたは列に移動できます。たとえば、線の上半分のみが現在のページであると判断した位置に線の上半分しか収まらない場合は、「ページネーションストラット」を挿入して、エンジンが次のページの最上部であると想定する位置に押し下げます。その後、実際の断片化作業(「ハサミと配置によるカット」)のほとんどは、レイアウトの後で、ペイント前およびペイント時にコンテンツを切り取ることにより、コンテンツを切り取ることによって行われます。これにより、断片化の変換や相対位置の適用(仕様で義務付けられているもの)など、いくつかのことが実質的に不可能になりました。さらに、以前のエンジンでもテーブルの断片化はサポートされていますが、Flex やグリッドの断片化はサポートされていません。

はさみ、配置、糊を使用する前の、従来エンジンで 3 列レイアウトが内部的にどのように表現されるかを示した図を以下に示します(高さが指定されているため、4 行しか収まらず、下部に余分なスペースがあります)。

コンテンツが改行されるページネーション ストラットを含む 1 つの列として内部で表現され、画面上では 3 つの列として表示される

従来のレイアウト エンジンは、レイアウト中にコンテンツを実際に断片化していないため、相対配置や変換が正しく適用されない、ボックスシャドウが列の端でクリップされるなど、多くの奇妙なアーティファクトが発生します。

text-shadow を使用した例を次に示します。

以前のエンジンでは、この処理が適切に行われません。

2 番目の列に配置された切り詰められたテキスト シャドウ。

最初の列の行の text-shadow がクリップされ、代わりに 2 番目の列の上部に配置されていることがわかります。これは、従来のレイアウト エンジンが断片化を認識しないためです。

これは次のように表示されます。

シャドウが正しく表示される 2 列のテキスト。

次に、変換と box-shadow を使用して、少し複雑にしましょう。従来のエンジンでは、クリッピングと列のオーバーフローが無効になっています。これは、変換が仕様上、レイアウト後、断片化後の効果として適用されることが想定されるためです。LayoutNG のフラグメンテーションでは、どちらも正しく動作します。これにより、Firefox との相互運用性が向上します。Firefox は、この分野のほとんどのテストが合格するなど、長い間分散化を適切にサポートしてきました。

ボックスが 2 つの列に分割されて表示される。

従来のエンジンは、縦長のモノリシックなコンテンツにも問題があります。複数のフラグメントに分割できないコンテンツはモノリシックです。オーバーフロー スクロールを使用する要素はモノリシックです。長方形以外の領域をスクロールすることはユーザーにとって意味がないためです。モノリシックなコンテンツの例としては、線ボックスや画像などがあります。次の例をご覧ください。

モノリシック コンテンツが高すぎて列内に収まらない場合、以前のエンジンはその部分を残してスライスします(スクロール可能なコンテナをスクロールしようとすると、非常に「興味深い」動作になります)。

最初の列をオーバーフローさせるのではなく(LayoutNG ブロックの断片化の場合のように):

ALT_TEXT_HERE

従来のエンジンは強制休憩をサポートしています。たとえば、<div style="break-before:page;"> は DIV の前に改ページを挿入します。ただし、最適な強制ではない改ページの検出は限定的にしかサポートされていません。break-inside:avoid孤立行と孤立段落はサポートされていますが、break-before:avoid でリクエストされた場合など、ブロック間の改行を回避することはできません。次の例を考えてみましょう。

テキストが 2 つの列に分割されている。

ここでは、#multicol 要素の各列に 5 行分のスペースがあります(高さが 100 ピクセルで、行の高さが 20 ピクセルであるため)。そのため、#firstchild のすべてを最初の列に収めることができます。ただし、兄弟 #secondchild には break-before:avoid があるため、コンテンツが 2 つのコンテンツの間に挿入点を設けないようにする必要があります。widows の値は 2 であるため、すべての改行回避リクエストを処理するには、2 行の #firstchild を 2 番目の列に push する必要があります。Chromium は、この機能の組み合わせを完全にサポートする最初のブラウザ エンジンです。

NG の断片化の仕組み

通常、NG レイアウト エンジンは、CSS ボックスツリーを深さ優先で走査してドキュメントをレイアウトします。ノードのすべての子孫がレイアウトされると、NGPhysicalFragment を生成して親レイアウト アルゴリズムに戻ることで、そのノードのレイアウトを完了できます。このアルゴリズムは、フラグメントを子フラグメントのリストに追加し、すべての子が完了すると、すべての子フラグメントを含むフラグメントを生成します。この方法では、ドキュメント全体のフラグメント ツリーが作成されます。ただし、これは単純化した説明です。たとえば、フロー外配置の要素は、レイアウトされる前に、DOM ツリー内の存在場所からその親ブロックにバブルアップする必要があります。ここでは、単純にするためにこの高度な詳細を無視しています。

LayoutNG は、CSS ボックス自体とともに、レイアウト アルゴリズムに制約空間を提供します。これにより、レイアウトに使用可能なスペース、新しいフォーマット コンテキストが確立されているかどうか、前のコンテンツからの中間マージンの折りたたみ結果などの情報がアルゴリズムに提供されます。制約空間には、フラグメンテーションのレイアウトされたブロックサイズと、そのブロックの現在のオフセットも含まれます。改行する場所を示します。

ブロックの断片化が関係する場合、子孫のレイアウトは中断したところで停止する必要があります。互換性が損なわれる理由としては、ページや列のスペース不足、強制的な中断などが考えられます。次に、アクセスしたノードのフラグメントを生成し、断片化コンテキストのルート(マルチコル コンテナ、または印刷の場合はドキュメントのルート)まで戻ります。次に、断片化コンテキストのルートで新しい断片化ツールの準備を行い、再びツリーに降りて、中断する前まで行った処理を再開します。

改行後にレイアウトを再開するための手段を提供する重要なデータ構造は NGBlockBreakToken と呼ばれます。次のフラグメンタイザでレイアウトを正しく再開するために必要な情報がすべて含まれています。NGBlockBreakToken はノードに関連付けられ、NGBlockBreakToken ツリーを形成します。これにより、再開が必要な各ノードが表されます。NGBlockBreakToken は、内部で分割されるノード用に生成された NGPhysicalBoxFragment に関連付けられます。ブレークトークンは親に伝播され、ブレークトークンのツリーを形成します。ノードの内部ではなくで分割する必要がある場合、フラグメントは生成されませんが、親ノードはノードの「break-before」ブレークトークンを作成する必要があります。これにより、次のフラグメンタイザのノードツリーで同じ位置に到達したときにレイアウトを開始できます。

フラグメントは、フラグメント スペースを使い切ったとき(強制適用されないブレーク)、または強制的なブレークがリクエストされたときに挿入されます。

仕様には、最適な強制ブレークに関するルールが定められており、スペースが不足している場所にブレークを挿入するだけでは、必ずしも適切な対応とは言えません。たとえば、break-before などのさまざまな CSS プロパティが、ブレークの位置の選択に影響します。

レイアウト中に、強制ブレークの仕様セクションを正しく実装するには、適切なブレークポイントを記録する必要があります。このレコードは、中断回避リクエスト(break-before:avoidorphans:7 など)に違反するポイントでスペースが不足した場合に、戻って最後に見つかった最適なブレークポイントを使用できることを意味します。考えられる各ブレークポイントには、スコアが割り当てられます。スコアは「最後の手段としてのみ行う」から「中断に最適な場所」まで、中間の値も含めて割り当てられます。改行位置のスコアが「完璧」の場合、その位置で改行しても改行ルールに違反しないことを意味します(スペースが不足する位置でこのスコアが得られた場合、より良い場所を探す必要はありません)。スコアが「最後の手段」の場合、ブレークポイントは有効なものでさえありませんが、より良いものが見つからなかった場合は、フラグメント オーバーフローを回避するために、ブレークポイントで中断する場合があります。

有効なブレークポイントは通常、兄弟(行ボックスまたはブロック)のにのみ存在し、親とその最初の子の間には存在しません(クラス C ブレークポイントは例外ですが、ここでは説明しません)。たとえば、break-before:avoid を指定したブロック シブレットの前に有効なブレークポイントはありますが、これは「最適」と「最後の手段」の中間程度です。

レイアウト中、NGEarlyBreak と呼ばれる構造でこれまでに見つかった最適なブレークポイントを追跡します。早期ブレークは、ブロックノード内またはブロックノードの前、または行(ブロック コンテナ行またはフレックス行)の前に設定できるブレークポイントです。NGEarlyBreak のオブジェクトのチェーンまたはパスを形成することができます。最適なブレークポイントは、スペースがなくなったときに以前歩いたものの中で深いところにあるかもしれません。次の例をご覧ください。

この場合、#second の直前にスペースが不足していますが、「break-before:avoid」が指定されているため、中断位置のスコアは「violating break avoid」になります。この時点で、NGEarlyBreak チェーンは「#outer 内 > #middle 内 > #inner 内 > 3 行目より前」となり、「完了」となるため、そこで中断することをおすすめします。そのため、#inner の「line 3」の前にブレークできるように、#outer の最初からレイアウトを再実行する必要があります(今回は検出された NGEarlyBreak を渡します)。(「3 行目」の前で改行されるため、残りの 4 行が次のフラグメントに入り、widows:4 が適用されるようになります)。

このアルゴリズムは、仕様で定義されているように、可能な限り最適なブレークポイントで常に中断するように設計されています。すべてのルールを満たすことができない場合は、ルールを正しい順序で破棄します。なお、再レイアウトはフラグメンテーション フローごとに最大 1 回だけ行う必要があります。2 回目のレイアウト パスの時点で、最適な改行位置はすでにレイアウト アルゴリズムに渡されています。これは、最初のレイアウト パスで検出された改行位置で、そのラウンドのレイアウト出力の一部として提供されます。2 回目のレイアウト パスでは、スペースが足りなくなるまでレイアウトしません。実際、スペースが足りなくなることは想定されていません(実際にはエラーになります)。これは、不要に改行ルールに違反しないように、早期に改行を挿入できる非常に優れた(利用可能な限り優れた)場所が用意されているためです。そのため、そのポイントまでレイアウトして、休憩します。

なお、フラグメンタイザー オーバーフローを回避するために、一部のブレーク回避リクエストを無視する必要がある場合があります。例:

ここでは、#second の直前にスペースが不足していますが、「break-before:avoid」が設定されています。これは、最後の例と同様に「violating break avoid」と翻訳されます。また、NGEarlyBreak で「violating orphans and widows」(#first 内 > 「line 2」の前に)という問題も発生しています。これはまだ完璧ではありませんが、「violating break avoid」よりはましです。そのため、「行 2」の前に改行され、孤立行 / 孤立行末のリクエストに違反します。仕様では、4.4 でこの問題に対処しています。強制ブレーク: フラグメンタイザー オーバーフローを回避するのに十分なブレークポイントがない場合に、最初に無視されるブレークルールを定義します。

まとめ

LayoutNG ブロックの断片化プロジェクトの機能的な目標は、以前のエンジンがサポートするすべての機能の LayoutNG アーキテクチャをサポートする実装を提供し、バグの修正以外は可能な限り少なくすることです。主な例外は、中断回避のサポートの改善(break-before:avoid など)です。これは断片化エンジンのコア部分であるため、後で追加すると再書き換えが必要になるため、最初から含まれている必要がありました。

LayoutNG ブロックの断片化が完了したので、印刷時の混合ページサイズのサポート、印刷時の @page 余白ボックス、box-decoration-break:clone などの新機能の追加に取り組むことができます。また、LayoutNG 全般と同様に、新しいシステムのバグ率とメンテナンスの負担は、時間の経過とともに大幅に低下することが予想されます。

謝辞

  • Una Kravets 様: 素敵な「手作りのスクリーンショット」をありがとうございます。
  • Chris Harrelson に校正、フィードバック、提案を依頼しました。
  • Philip Jägenstedt までご連絡ください。
  • Rachel Andrew 様(編集と最初の複数列の例の図)。