ブロックの断片化は、CSS ブロックレベルのボックス(セクションや段落など)が 1 つのフラグメント コンテナ(fragmentainer)に全体として収まらない場合に、そのボックスを複数のフラグメントに分割します。フラグメンタイナーは要素ではなく、複数列レイアウトの列またはページド メディアのページを表します。
断片化を行うには、コンテンツが断片化コンテキスト内にある必要があります。断片化コンテキストは、最も一般的にはマルチ列コンテナ(コンテンツが列に分割される)または印刷時(コンテンツがページに分割される)に確立されます。行数の多い長い段落は、最初の行を最初のフラグメントに配置し、残りの行を後続のフラグメントに配置するように、複数のフラグメントに分割する必要があります。

ブロックの断片化は、よく知られている別のタイプの断片化である行の断片化(「行の分割」とも呼ばれます)に似ています。複数の単語で構成され、改行が許可されるインライン要素(任意のテキストノード、任意の <a>
要素など)は、複数のフラグメントに分割できます。各フラグメントは別の行ボックスに配置されます。行ボックスは、列とページのフラグメンタイザーと同等のインライン フラグメンテーションです。
LayoutNG ブロックの断片化
LayoutNGBlockFragmentation は、LayoutNG のフラグメンテーション エンジンの書き換えであり、最初は Chrome 102 でリリースされました。データ構造に関しては、複数の NG 前のデータ構造を、フラグメント ツリーで直接表される NG フラグメントに置き換えました。
たとえば、CSS プロパティ「break-before」と「break-after」の「avoid」値がサポートされるようになりました。これにより、ヘッダーの直後に改行しないようにすることができます。ページの最後がヘッダーで、セクションのコンテンツが次のページから始まると、見栄えが悪くなることがよくあります。ヘッダーの前に改行することをおすすめします。

Chrome は断片化オーバーフローもサポートしているため、モノリシックな(分割できないはずの)コンテンツが複数の列にスライスされず、シャドウや変換などのペイント エフェクトが正しく適用されます。
LayoutNG のブロックの断片化が完了しました
コア フラグメンテーション(ブロック コンテナ、行レイアウト、フロート、フロー外配置など)は Chrome 102 でリリースされました。フレックスとグリッドのフラグメンテーションは Chrome 103 でリリースされ、テーブルのフラグメンテーションは Chrome 106 でリリースされました。最後に、Chrome 108 で印刷がリリースされました。ブロックの断片化は、レイアウトの実行にレガシー エンジンに依存する最後の機能でした。
Chrome 108 以降、レイアウトの実行にレガシー エンジンは使用されなくなりました。
また、LayoutNG データ構造はペイントとヒットテストをサポートしていますが、レイアウト情報を読み取る JavaScript API では、offsetLeft
や offsetTop
などの従来のデータ構造に依存しています。
NG ですべてをレイアウトすることで、LayoutNG でのみ実装されている(以前のエンジンに相当するものがない)新しい機能を実装してリリースできるようになります。たとえば、CSS コンテナ クエリ、アンカーの配置、MathML、カスタム レイアウト(Houdini)などです。コンテナ クエリについては、印刷はまだサポートされていないという警告をデベロッパーに表示しつつ、少し前にリリースしました。
LayoutNG の最初の部分は 2019 年にリリースされました。これには、通常のブロック コンテナ レイアウト、インライン レイアウト、フロート、フロー外のポジショニングが含まれていましたが、フレックス、グリッド、テーブルはサポートされておらず、ブロックの断片化はまったくサポートされていませんでした。フレックス、グリッド、テーブル、ブロックの断片化に関連するすべての機能については、従来のレイアウト エンジンが使用されます。これは、断片化されたコンテンツ内のブロック要素、インライン要素、フローティング要素、アウトオブフロー要素でも同様でした。このように複雑なレイアウト エンジンをインプレースでアップグレードするのは、非常にデリケートな作業です。
また、2019 年半ばまでに、LayoutNG ブロックの断片化レイアウトのコア機能の大部分が(フラグで)実装されています。発送に時間がかかったのはなぜですか?端的に答えると、断片化はシステムのさまざまなレガシー部分と正しく共存する必要があります。これらのレガシー部分は、すべての依存関係がアップグレードされるまで削除またはアップグレードできません。
以前のエンジンとのやり取り
レガシー データ構造は、レイアウト情報を読み取る JavaScript API を引き続き担当しているため、レガシー エンジンが理解できる方法でデータを書き戻す必要があります。これには、LayoutMultiColumnFlowThread などの従来のマルチカラム データ構造を正しく更新することも含まれます。
以前のエンジンのフォールバック検出と処理
LayoutNG ブロックの断片化でまだ処理できないコンテンツが内部にある場合は、以前のレイアウト エンジンにフォールバックする必要がありました。コア LayoutNG ブロックの断片化がリリースされた時点では、これには flex、グリッド、表、印刷されるものすべてが含まれていました。これは特に難しい作業でした。レイアウト ツリー内のオブジェクトを作成する前に、以前の代替手段の必要性を検出する必要があったためです。たとえば、マルチ列コンテナの祖先があるかどうか、どの DOM ノードがフォーマット コンテキストになるかどうかを検出する前に、検出する必要がありました。これは、完全な解決策がないニワトリと卵の問題ですが、誤動作が誤検出(実際には必要がないときに従来版にフォールバックする)のみである限り、問題ありません。そのレイアウト動作のバグは、Chromium にすでに存在するものであり、新しいものではありません。
ペイント前の木の散歩道
ペイント前は、レイアウト後でペイント前に行います。主な課題は、レイアウト オブジェクト ツリーを走査する必要があるにもかかわらず、NG フラグメントが存在することです。この問題をどのように解決すればよいでしょうか。レイアウト オブジェクトと NG フラグメント ツリーの両方を同時に走査します。2 つのツリー間のマッピングは簡単ではないため、これは非常に複雑です。
レイアウト オブジェクトのツリー構造は DOM ツリーとよく似ていますが、フラグメント ツリーはレイアウトの入力ではなく、レイアウトの出力です。フラグメント ツリーは、インライン フラグメンテーション(行フラグメント)やブロック フラグメンテーション(列またはページ フラグメント)などのフラグメンテーションの効果を実際に反映するだけでなく、包含ブロックと、そのフラグメントを包含ブロックとする DOM 子孫の間に直接の親子関係も持ちます。たとえば、フラグメント ツリーでは、絶対配置要素によって生成されたフラグメントは、フロー外配置の末子とその親ブロックの間に他のノードが存在する場合でも、その親ブロック フラグメントの直接の子になります。
フラグメンテーション内にアウトフローの位置要素がある場合は、さらに複雑になる可能性があります。これは、アウトフローのフラグメントがフラグメンタイナーの直接の子になり(CSS がコンテナ ブロックと見なすものの子ではなく)、これは、以前のエンジンと共存するために解決する必要があった問題でした。LayoutNG は、最新のすべてのレイアウト モードを柔軟にサポートするように設計されているため、将来的にはこのコードを簡素化できるはずです。
従来のフラグメンテーション エンジンの問題
ウェブの初期に設計されたレガシー エンジンには、(印刷をサポートするために)技術的には当時も存在していたものの、断片化の概念が実際にはありません。断片化のサポートは、上からボルト止めされたもの(印刷)または後付けされたもの(複数列)でした。
フラグメント化可能なコンテンツをレイアウトする場合、従来のエンジンは、すべてを幅が列またはページの行内サイズで高さがコンテンツを収める必要のある高さの長いストリップにレイアウトします。この長いストリップはページにレンダリングされません。仮想ページにレンダリングされ、最終的な表示のために再配置されます。概念的には、新聞記事を 1 つの列に印刷し、次にハサミで複数にカットするのと似ています。(昔、一部の新聞では実際にこのような手法が使われていました)。
従来のエンジンは、ストリップ内の仮想のページまたは列の境界を追跡します。これにより、境界を超えて収まらないコンテンツを次のページまたは列に移動できます。たとえば、エンジンが現在のページであると判断したページに収まるのは行の上部半分だけの場合は、「ページネーション ストラット」を挿入して、エンジンが次のページの上部であると判断する位置まで押し下げます。実際の断片化作業(「はさみで切って配置する」作業)のほとんどは、レイアウト後のプリペイントとペイント中に、コンテンツの長いストリップをページまたは列にスライスすることで行われます(部分をクリップして移動します)。これにより、断片化後の変換や相対位置の適用(仕様で義務付けられているもの)など、いくつかのことが実質的に不可能になりました。さらに、従来のエンジンではテーブルの断片化は一部サポートされていますが、フレックスやグリッドの断片化はまったくサポートされていません。
はさみ、配置、糊を使用する前の、従来エンジンで 3 列レイアウトが内部的にどのように表現されるかを示した図を以下に示します(高さが指定されているため、4 行しか収まらず、下部に余分なスペースがあります)。

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

最初の列の行の text-shadow がクリップされ、代わりに 2 番目の列の上部に配置されていることがわかります。これは、従来のレイアウト エンジンが断片化を認識しないためです。
これは次のように表示されます。
次に、トランスフォームと box-shadow を使って、少し複雑なデザインにしましょう。従来のエンジンでは、クリッピングと列のオーバーフローが無効になっています。これは、変換は仕様上、レイアウト後、フラグメンテーション後のエフェクトとして適用されるためです。LayoutNG のフラグメンテーションでは、どちらも正常に動作します。これにより、Firefox との相互運用性が向上します。Firefox は、この分野のほとんどのテストが合格するなど、長い間分散化を適切にサポートしてきました。
従来のエンジンは、縦長のモノリシックなコンテンツにも問題があります。コンテンツを複数のフラグメントに分割できない場合は、コンテンツはモノリシックです。オーバーフロー スクロールを使用する要素はモノリシックです。長方形以外の領域をスクロールすることはユーザーにとって意味がないためです。モノリシックなコンテンツの例としては、線ボックスや画像などがあります。次の例をご覧ください。
モノリシックなコンテンツの一部が列内に収まらないほど高すぎる場合、従来のエンジンはそれを無慈悲にスライスします(スクロール可能なコンテナをスクロールしようとすると、非常に「興味深い」動作になります)。
(LayoutNG のブロック断片化の場合のように)最初の列にオーバーフローさせません。
従来のエンジンは強制休憩をサポートしています。たとえば、<div style="break-before:page;">
は DIV の前に改ページを挿入します。ただし、最適な強制ではない改ページの検出は限定的にしかサポートされていません。break-inside:avoid
と孤立行と孤立段落はサポートされていますが、break-before:avoid
でリクエストされた場合など、ブロック間の改行を回避することはできません。次の例を考えてみましょう。
ここでは、#multicol
要素の各列に 5 行分のスペースがあります(高さが 100 ピクセルで、行の高さが 20 ピクセルであるため)。そのため、#firstchild
のすべてを最初の列に収めることができます。ただし、その兄弟の #secondchild
には break-before:avoid が指定されています。つまり、コンテンツは、その間に区切りが入らないようにすることを望んでいます。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:avoid
や orphans:7
など)に違反するポイントでスペースが不足した場合に、戻って最後に見つかった最適なブレークポイントを使用できることを意味します。各可能なブレークポイントには、「最後の手段としてのみ行う」から「ブレークに最適な場所」までのスコアが付けられます。改行位置のスコアが「完璧」の場合、その位置で改行しても改行ルールに違反しないことを意味します(スペースが不足する位置でこのスコアが得られた場合、より良い場所を探す必要はありません)。スコアが「last-resort」の場合、ブレークポイントは有効ではありませんが、フラグメンタイザー オーバーフローを回避するために、これより優れたものが見つからない場合は、そこでブレークすることがあります。
有効なブレークポイントは通常、兄弟(行ボックスまたはブロック)間にのみ存在し、親とその最初の子の間には存在しません(クラス C ブレークポイントは例外ですが、ここでは説明しません)。たとえば、break-before:avoid を指定したブロック シブレットの前に有効なブレークポイントはありますが、これは「最適」と「最後の手段」の中間程度です。
レイアウト中に、これまでに検出された最適なブレークポイントを NGEarlyBreak という構造に記録します。早期ブレークは、ブロックノード内またはブロックノードの前、または行(ブロック コンテナ行またはフレックス行)の前に設定できるブレークポイントです。最適なブレークポイントが、スペースが不足したときにすでに通過した内部のどこかにある場合を考慮して、NGEarlyBreak オブジェクトのチェーンまたはパスを形成することがあります。次の例をご覧ください。
この場合、#second
の直前にスペースが不足していますが、「break-before:avoid」が指定されているため、中断位置のスコアは「violating break avoid」になります。この時点で、NGEarlyBreak チェーンは「#outer
内 > #middle
内 > #inner
内 > 3 行目より前」となり、「完璧」であるため、そこで中断することをおすすめします。そのため、#inner の「line 3」の前にブレークできるように、#outer の最初からレイアウトを再実行する必要があります(今回は検出された NGEarlyBreak を渡します)。(widows:4
を尊重するため、残りの 4 行が次のフラグメンタイナに配置されるように、「行 3」の前に改行します)。
このアルゴリズムは、仕様で定義されているように、可能な限り最適なブレークポイントで常に中断するように設計されています。すべてのルールを満たすことができない場合は、ルールを正しい順序で破棄します。なお、再レイアウトはフラグメンテーション フローごとに最大 1 回のみ行う必要があります。2 回目のレイアウト パスの時点では、最適な改行位置はすでにレイアウト アルゴリズムに渡されています。これは、1 回目のレイアウト パスで検出され、そのラウンドのレイアウト出力の一部として提供された改行位置です。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 様(編集と最初の複数列の例の図)