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

レイアウトは、要素の幾何学的情報(要素のサイズやページ内での位置)をブラウザが把握する場所です。各要素には、使用された 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 ミリ秒かかるのに、これは大きすぎます。また、DevTools にはツリーサイズ(この場合は 1,618 個の要素)と、レイアウトが必要なノードの数(この場合は 5 個)も表示されます。

ここでの一般的なアドバイスは、可能な限りレイアウトを避けることですが、常にレイアウトを回避できるとは限りません。レイアウトを避けられない場合、レイアウトの費用は DOM のサイズに関係しています。両者の関係は密結合ではありませんが、一般的に 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 のヒーロー画像