レイアウトは、要素の幾何学的情報(要素のサイズとページ上の位置)をブラウザによって管理する場所です。各要素には、使用された CSS、要素の内容、親要素に基づいて、明示的または暗黙的なサイズ設定情報が含まれます。このプロセスは、Chrome ではレイアウトと呼ばれます。
レイアウトは、要素の幾何学的情報(要素のサイズ、ページ上での位置)をブラウザで管理する場所です。個々の要素は、使用された CSS、要素の内容、親要素に基づいて、明示的または暗黙的なサイジング情報を持ちます。このプロセスは、Chrome(および Edge などの派生ブラウザ)と Safari では Layout と呼ばれます。Firefox ではリフローと呼ばれますが、実質上は同じプロセスです。
スタイルの計算と同様に、レイアウトのコストについて次の点を直接考慮する必要があります。
- レイアウトを必要とする要素の数。これは、ページの DOM サイズの副産物です。
- それらのレイアウトの複雑性
概要
- レイアウトはインタラクション レイテンシに直接影響します
- 通常、レイアウトはドキュメント全体に適用されます。
- 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] タブに移動し、記録ボタンをタップしてサイトを操作します。記録を停止すると、サイトのパフォーマンスの詳細が表示されます。
上記の例のトレースを確認すると、各フレームでレイアウト内部で 28 ミリ秒超の時間が費やされています。アニメーションでフレームを画面に表示する時間が 16 ミリ秒であるのと比べて、これは長すぎます。また、DevTools では、ツリーのサイズ(この例では 1,618 要素)と、レイアウトで必要とされたノードの数(この例では 5)もわかります。
レイアウトは可能な限り避けるのが一般的ですが、レイアウトを避けられないこともあります。レイアウトを避けられない場合、レイアウトのコストは DOM のサイズに関係しています。両者の関係は密結合ではありませんが、一般的に DOM が大きいほどレイアウトのコストは高くなります。
強制同期レイアウトを回避する
フレームを画面に配置する順序は次のとおりです。
最初に JavaScript を実行し、次にスタイル計算、その次にレイアウトを実行します。ただし、JavaScript を使用することで、ブラウザによる早期のレイアウト実行を強制することは可能です。これは強制同期レイアウトと呼ばれます。
まず注意すべきなのは、JavaScript が実行されると、前のフレームの古いレイアウト値がすべて認識され、クエリで使えるようになることです。したがって、たとえば、フレームの最初に要素(例: ボックス)の高さを書き出す場合は、次のようなコードを作成できます。
// 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.
const width = box.offsetWidth;
function resizeAllParagraphsToMatchBlockWidth () {
for (let i = 0; i < paragraphs.length; i++) {
// Now write.
paragraphs[i].style.width = `${width}px`;
}
}
安全性を保証したい場合は、FastDOM の使用を検討してください。FastDOM は読み取りと書き込みを自動的にバッチ処理し、レイアウトの強制同期やレイアウト スラッシングを誤ってトリガーすることを回避します。