大規模で複雑なレイアウトとレイアウトのスラッシングを回避する

レイアウトでは、要素の幾何学的情報(要素のサイズやページ内での位置)をブラウザが把握します。各要素には、使用された CSS、要素の内容、親要素に基づく、明示的または暗黙的なサイズ情報が含まれます。このプロセスは Chrome では「レイアウト」と呼ばれます。

レイアウトは、要素の幾何学的情報(要素のサイズ、ページ内での位置)をブラウザが把握する場所です。各要素には、使用された CSS、要素の内容、親要素に基づく、明示的または暗黙的なサイズ情報が含まれます。このプロセスは、Chrome(および Edge などの派生ブラウザ)、Safari ではレイアウトと呼ばれます。Firefox ではリフローと呼ばれますが、プロセスは実質的に同じです。

スタイルの計算と同様に、レイアウト コストに関する差し迫った問題は次のとおりです。

  1. レイアウトを必要とする要素の数(レイアウトはページの DOM サイズによって左右されます)。
  2. それらのレイアウトの複雑さ。

まとめ

  • レイアウトはインタラクションのレイテンシに直接影響する
  • 通常、レイアウトのスコープはドキュメント全体に設定されます。
  • DOM 要素の数はパフォーマンスに影響するため、可能な限りレイアウトのトリガーは避けてください。
  • 強制的な同期レイアウトとレイアウト スラッシングを回避します。スタイル値を読み取ってから、スタイルを変更します。

インタラクション レイテンシに対するレイアウトの影響

ユーザーがページを操作するとき、その操作はできるだけ速く行う必要があります。インタラクションが完了するまでに要する時間(ブラウザがインタラクションの結果を表示するために次のフレームを表示したとき)を、インタラクション レイテンシといいます。これは、Interaction to Next Paint 指標で測定されるページ パフォーマンスの要素です。

ユーザー操作に対してブラウザが次のフレームを表示するのにかかる時間を、操作の表示遅延といいます。インタラクションの目的は、何かが発生したことをユーザーに知らせるために視覚的なフィードバックを提供することです。視覚的な更新では、その目標を達成するために、ある程度のレイアウト作業が必要になる場合があります。

ウェブサイトの INP を可能な限り低く抑えるには、可能な限りレイアウトを避けることが重要です。レイアウトを完全に回避することができない場合は、ブラウザが次のフレームをすばやく表示できるように、レイアウトの処理を制限することが重要です。

可能な限りレイアウトを避ける

スタイルを変更すると、ブラウザは、変更によってレイアウトの計算が必要かどうかと、そのレンダー ツリーを更新する必要があるかどうかを確認します。幅、高さ、左、上などの「幾何学的プロパティ」に対する変更はすべてレイアウトを必要とします。

.box {
  width: 20px;
  height: 20px;
}

/**
  * Changing width and height
  * triggers layout.
  */

.box--expanded {
  width: 200px;
  height: 350px;
}

ほとんどの場合、レイアウトのスコープはドキュメント全体に設定されます。要素が多い場合、すべての要素の場所と寸法を把握するには長い時間がかかります。

レイアウトを回避できない場合は、もう一度 Chrome DevTools を使用して所要時間を確認し、レイアウトがボトルネックの原因かどうかを判断することが重要です。まず、DevTools を開いて [Timeline] タブに移動し、[記録] をクリックしてサイトを操作します。記録を停止すると、サイトのパフォーマンスの内訳が表示されます。

レイアウトに長時間が表示されている DevTools。

上記の例のトレースを詳しく調べると、各フレームのレイアウト内で 28 ミリ秒以上が費やされていることがわかります。アニメーションのフレームを画面に表示するのに 16 ミリ秒かかる場合、これはかなり大きすぎます。また、ツリーのサイズ(この場合は 1,618 要素)と、レイアウトが必要なノードの数(この場合は 5)も表示されます。

ここでの一般的なアドバイスは、可能な限りレイアウトを避けることですが、必ずしもレイアウトを回避できるとは限りません。レイアウトを避けられない場合は、レイアウトのコストと DOM のサイズに関係があることに注意してください。この 2 つの関係は密結合ではありませんが、DOM が大きくなると、通常はレイアウト コストが高くなります。

強制同期レイアウトを回避する

フレームを画面に配送する際の順序は次のとおりです。

Flexbox をレイアウトとして使用する。

最初に JavaScript が実行され、次にスタイルの計算、レイアウトが実行されます。ただし、JavaScript を使用してブラウザにレイアウトを早めに実行させることができます。これは強制同期レイアウトと呼ばれます。

まず注意すべき点は、JavaScript が実行すると、前のフレームの古いレイアウト値がすべて把握され、クエリに使用できるということです。たとえば、フレームの先頭で要素の高さ(「box」と呼ぶ)を書き出す場合は、次のようなコードを記述できます。

// Schedule our function to run at the start of the frame:
requestAnimationFrame(logBoxHeight);

function logBoxHeight () {
  // Gets the height of the box in pixels and logs it out:
  console.log(box.offsetHeight);
}

ボックスの高さをリクエストする前にボックスのスタイルを変更すると、問題が発生します。

function logBoxHeight () {
  box.classList.add('super-big');

  // Gets the height of the box in pixels and logs it out:
  console.log(box.offsetHeight);
}

ここで、高さの質問に答えるには、ブラウザはまずスタイル変更を適用(super-big クラスを追加しているため)してから、レイアウトを実行する必要があります。そうして初めて、正しい高さを返すことができます。これは不必要な作業であり、場合によってはコストがかかります。

そのため、スタイルの読み取りは常にバッチ処理し、最初に(ブラウザが前のフレームのレイアウト値を使用できる)読み取り処理を行ってから、書き込み処理を行う必要があります。

正しく実行すると、上記の関数は次のようになります。

function logBoxHeight () {
  // Gets the height of the box in pixels and logs it out:
  console.log(box.offsetHeight);

  box.classList.add('super-big');
}

ほとんどの場合、スタイルを適用してから値をクエリする必要はありません。最後のフレームの値を使用すれば十分です。スタイル計算とレイアウトをブラウザが望むよりも早く同期的に実行することはボトルネックになり得るため、通常は実行したいことではありません。

レイアウト スラッシングを回避する

強制同期レイアウトをさらに不便にさせる方法があります。それは、多数の強制同期レイアウトを連続して実行することです。次のコードをご覧ください。

function resizeAllParagraphsToMatchBlockWidth () {
  // Puts the browser into a read-write-read-write cycle.
  for (let i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = `${box.offsetWidth}px`;
  }
}

このコードは段落のグループをループ処理し、各段落の幅を「box」という要素の幅に合わせて設定します。無害に見えますが、問題は、ループの各反復処理がスタイル値(box.offsetWidth)を読み取り、すぐにそれを使用して段落の幅を更新することです(paragraphs[i].style.width)。ループの次の反復処理では、offsetWidth が前回(前の反復処理で)リクエストされてからスタイルが変更されたことを考慮する必要があるため、スタイル変更を適用してレイアウトを実行する必要があります。これは、反復処理のたびに発生します。

このサンプルを修正するには、もう一度値を read してから、write します。

// Read.
const width = box.offsetWidth;

function resizeAllParagraphsToMatchBlockWidth () {
  for (let i = 0; i < paragraphs.length; i++) {
    // Now write.
    paragraphs[i].style.width = `${width}px`;
  }
}

安全性を保証したい場合は、FastDOM の使用を検討してください。FastDOM は読み取りと書き込みを自動的にバッチ処理し、レイアウトの強制同期やレイアウト スラッシングが誤ってトリガーされないようにします。

Hal Gatewood による Unsplash のヒーロー画像。