プロンプトと適用されるコンテキスト
ダッシュボードに300枚のカードがレンダリングされています。スクロールやリサイズ中、あるハンドラーが各カードのバウンディングボックスを読み取り、幅と位置を変更してから高さを読み取ります。インターフェースにカクつきが発生します。ChromeのPerformanceパネルの記録では、ハンドラー内で紫色のLayoutイベントと強制リフローの警告が繰り返し発生していることが示されています。
ブラウザの JavaScript → style → layout → paint → composite パイプラインを説明し、このコードがレイアウトスラッシング(Layout Thrashing)を引き起こしているかどうかを証明し、古いジオメトリを使用せずにリファクタリングして、検証計画を定義してください。カードの枚数やトレースの症状は面接用の前提条件であり、実際のプロダクトの観測値ではありません。
これはフロントエンドのパフォーマンス診断に関する質問です。中核となるスキルは、無効化(invalidation)と同期ジオメトリ読み取りをトレースの証拠および安全なコード変更へと結びつけることです。これはフィールドメトリクスから開始するCore Web Vitalsの調査よりも範囲が狭く、ネットワークや初期レンダリング段階を対象とする新しいURLナビゲーションのトレースとも異なります。
面接官が評価するポイント
第一に、候補者が無効化と実行を区別できるかどうかです。DOMやスタイルの書き込みは、ジオメトリを即座に再計算することなく、スタイルやレイアウトを「ダーティ(dirty)」としてマークすることがあります。その後に現在のジオメトリを返す必要があるAPIが呼ばれると、ブラウザは保留中のスタイルとレイアウトを同期的にフラッシュ(即時実行)せざるを得なくなります。
第二に、候補者がスラッシングを「遅いプロパティ」のリストとしてではなく、依存関係のパターンとして説明できるかどうかです。1回の書き込みの後に必要な読み取りが1回行われる構成は正当な場合があります。有害なパターンは、多数の要素やフレームにわたって「書き込み → レイアウト依存の読み取り → 書き込み」を繰り返すことであり、ブラウザによる処理の統合(coalescing)を妨げます。
第三に、候補者が最適化の前に診断を行えるかどうかです。優れた回答では、実際のインタラクションを記録し、Layoutイベントのイニシエータとコールスタックを確認し、スクリプト、スタイル、レイアウト、ペイントに費やされた時間を比較して、疑わしいハンドラーが原因経路にあることを確認します。紫色のバーが存在するだけでは、すべてのレイアウトが回避可能であることの証明にはなりません。
第四に、候補者が正確性を維持できるかどうかです。すべての読み取りを先頭に移動する手法は、それらの測定値がアルゴリズムの必要とする状態を表している場合にのみ機能します。各書き込みが意図的に次の測定結果を変化させる構造である場合、アルゴリズムまたはデータモデルを変更する必要があります。ジオメトリを盲目的にキャッシュすると、高速ではあっても誤ったレイアウトが生成されます。
最後に、候補者が実際の依存関係に基づいて、バッチ処理、requestAnimationFrame、オブザーバー、CSSレイアウト、包含(containment)、コンポジターに適したプロパティの中から適切なものを選択できるかどうかです。requestAnimationFrameはタイミングを変更するだけで負荷の高い処理を無償化するわけではなく、will-changeはレイアウトの汎用的な修復手段ではありません。
回答前に明確にすべき質問
- どのインタラクションが遅いのか? スクロール、リサイズ、初回レンダリング、ドラッグ、一度限りの展開では、許容予算やスケジューリングの選択肢が異なります。基準は継続的なスクロールとリサイズです。
- どのような読み取りと書き込みが発生しているか? ジオメトリの読み取りには
getBoundingClientRect()、offsetWidth、offsetHeightなどが含まれ、書き込みはクラス、インラインスタイル、コンテンツ、DOM構造を変更する場合があります。APIの名前だけでなく、正確な順序が重要です。 - あるカードの新しいサイズが別のカードの測定結果を決定するか? 決定しない場合、通常は1つの安定した状態からすべての測定値を読み取ることができます。決定する場合、アルゴリズムには単純なバッチ処理では維持できないシーケンシャルな依存関係が存在します。
- CSSにレイアウトを任せることは可能か? Grid、flexbox、コンテナクエリ、組み込みのサイジングにより、JavaScriptによる測定を完全に排除できる場合があります。これは多くの場合、測定ループを最適化するよりも強力です。
- 入力は他の場所で変更されていないか? フレームワークのコミット、画像、フォント、サードパーティ製ウィジェット、またはオブザーバーが、フェーズ間でレイアウトを無効化する可能性があります。修正には単一の所有者または明確なスケジューリングプロトコルが必要です。
- 視覚的に同一を保つべき要素は何か? 実装を変更する前に、カードの位置、サイズ、フォーカス動作、スクロールアンカリング、リサイズへの応答を定義します。
- 対象となる環境は何か? 影響を受けるビューポートおよびデバイスクラスで再現します。高速な開発用ノートPCでは、低スペックなCPUで破綻するレイアウトの繰り返しが見落とされる可能性があります。
30秒の回答フレームワーク
「まず、正確なスクロールまたはリサイズのインタラクションを記録し、繰り返されるLayoutイベントを選択してそのイニシエータとコールスタックを特定します。スタイルの書き込みはジオメトリを無効化します。その後の getBoundingClientRect() や offsetHeight は現在の値を返す必要があるため、ブラウザはスタイルとレイアウトを同期的にフラッシュする可能性があります。そのシーケンスを300枚のカードに対して繰り返すことがレイアウトスラッシングです。すべてのカードが更新前の同一状態を使用できる場合は、まず必要なジオメトリをすべて読み取り、メモリ内で計算し、次の視覚的更新時にまとめて書き込みを実行します。JavaScriptのポーリングが不要な場合は、CSSレイアウトまたはオブザーバーを優先します。その後、同じ決定論的なインタラクションを再生し、レイアウト回数、レイアウト所要時間、フレームのギャップ、視覚的な正確性を比較します。requestAnimationFrame だけで重複処理が解消されるとは主張しません。」
ステップごとの詳細解説
ステップ 1: 因果関係モデルを構築する
フレームには、JavaScript、スタイル計算、レイアウト、ペイント、コンポジットが含まれる場合があります。レイアウトはボックスのジオメトリを計算します。ジオメトリを変更する書き込みは、その情報の一部を古いもの(stale)としてマークします。ブラウザは多くの場合、複数の変更をまとめて処理できるように再計算を延期します。
同期的なジオメトリの読み取りは、このスケジュールを変更します。正しい現在値を返すために、ブラウザは保留中のスタイル変更を適用し、即座にレイアウトを実行する必要が生じます。別の書き込みが行われた後、次の読み取りによって再びレイアウトが強制される可能性があります。300枚のカードがあると、交互に繰り返されるループによって、1つのハンドラー内でドキュメント全体または部分的な再計算が多数発生することがあります。
有用なルールは、「既知の1つの視覚状態から読み取り、DOMに触れずに計算し、次の状態をまとめて書き込む」ことです。これは依存関係のルールであり、すべての読み取りや書き込みが高コストであるという保証ではありません。
ステップ 2: ハンドラーが原因であることを証明する
同じカードデータ、ビューポート、スクロール距離またはリサイズシーケンス、CPU条件など、決定論的なアクションに基づいてPerformance記録を取得します。FramesトラックとMainトラックをインスペクトします。時間の長いLayoutイベントを選択し、「initiated by」またはスタックを追跡してアプリケーションコードを特定します。1つのハンドラー内でいくつのレイアウトイベントが発生し、どれだけの時間を消費しているかを確認します。
トレースが混雑している場合は、ハンドラーの前後に一時的なパフォーマンスマークを追加します。ペイントフラッシングやレイヤー境界は補助的な証拠としてのみ使用してください。これらは再描画領域やレイヤーを可視化しますが、強制レイアウトとコードを結びつけるのはPerformanceトレースです。また、スクリプト実行時間とペイント時間も記録してください。レイアウトを排除しても、無関係な計算や巨大な再描画に支配されているハンドラーは改善されません。
対照実験(コントロール)を作成します。データとインタラクションはそのまま残し、疑わしい読み書きループのみを無効化します。繰り返されるLayoutイベントとフレームのギャップが解消されれば、因果関係の主張が強化されます。解消されない場合は、好みの診断を無理に当てはめるのではなく、フレームワークのコミット、画像のサイズ調整、フォント、サードパーティコードを調査します。
ステップ 3: 独立した測定をフェーズごとにリファクタリングする
元のハンドラーが読み取りと書き込みを交互に行っていると仮定します。
function positionCards(container, cards, columns) {
let top = 0;
for (const card of cards) {
const width = container.getBoundingClientRect().width / columns;
card.style.width = `${Math.floor(width)}px`;
const height = card.offsetHeight;
card.style.transform = `translateY(${top}px)`;
top += height;
}
}ここでは、各高さは正当に新しい幅に依存しているため、すべての古い高さをキャッシュするのは誤りです。コードを300回交互に実行するのではなく、2つの安定した視覚状態に分割します。
function positionCards(container, cards, columns) {
const width = Math.floor(container.getBoundingClientRect().width / columns);
for (const card of cards) {
card.style.width = `${width}px`;
}
requestAnimationFrame(() => {
const heights = cards.map((card) => card.offsetHeight);
let top = 0;
cards.forEach((card, index) => {
card.style.transform = `translateY(${top}px)`;
top += heights[index];
});
});
}最初の読み取りでは、幅変更前のコンテナを観察します。すべての幅の書き込みはグループ化されます。最初の高さの読み取りでは新しい幅に対する1回の必要なレイアウトが発生する可能性がありますが、残りの高さの読み取りはその安定した幅変更後のジオメトリを再利用します。その後、別のジオメトリ読み取りを行わずに transform が書き込まれます。アニメーションフレームのコールバックは第2フェーズを調整しますが、改善の本質はコールバック名にあるのではなく、何百もの交互の依存関係を2つの明示的な状態に削減したことにあります。
スクロールやリサイズの通知を結合(Coalesce)し、1フレームあたり最大1つの保留中更新のみが存在するようにします。すべてのイベントが別のコールバックをキューに追加すると、アプリケーションは未処理の負荷を移動させたに過ぎません。最新の入力を保持し、スケジュールを1回設定し、コールバックが実行されたときに保留フラグをクリアします。
ステップ 4: 真正なシーケンシャル依存関係を処理する
カードAの書き込みが、カードBで読み取る必要のあるジオメトリを意図的に変更する場合、バッチ処理は無効です。すべての測定が独立しているかのように見せかけるのではなく、その制約を明示してください。可能な修正方法としては、累積的なインメモリモデルから各位置を導出する、CSS Gridやflexboxにフローレイアウトを任せる、各子要素ではなく1つのコンテナを測定する、または以前にコミットされたフレームのみを必要とするようにエフェクトを再設計する、などが挙げられます。
コンテンツのサイズが非同期に変更される場合、ResizeObserver を使用すると、毎スクロールイベントをポーリングすることなくサイズ変更を検知できます。そのコールバックでもリサイズのフィードバックループの作成を避ける必要があります。提供された監視結果から計算し、書き込みをバッチ処理し、収束ルールなしに同一の監視対象ボックスのサイズを繰り返し変更しないようにします。
CSS containment(包含)は、コンポーネントの境界が真に独立している場合に、レイアウトやペイントの無効化が伝播する範囲を狭めることができます。ただし、組み込みのサイジング、オーバーフロー、包含ブロック(containing-block)の動作が変更される可能性があるため、外観とアクセシビリティを検証してください。無効化のスコープを小さくするとコストは削減されますが、繰り返される強制レイアウトを正当化するものではありません。
ステップ 5: セマンティクスが許容する場合にのみ低負荷な視覚変更を選択する
width や top などの幾何学的プロパティを変更するには、通常、レイアウト、ペイント、コンポジットが必要です。transform や opacity の変更は、レイアウトやペイントを回避してコンポジットに直接進むことができる場合があります。ドキュメントフローが新しいジオメトリを必要としない視覚的な移動には、このパスを使用します。
兄弟要素、ヒットテスト領域、テキストの折り返し、またはアクセシビリティのジオメトリが新しいサイズを反映する必要がある場合、実際の幅の変更を scale 変換で置き換えてはなりません。同様に、過度なレイヤー昇格(layer promotion)はメモリを消費します。まず意図したレイアウトセマンティクスを確認し、最も低負荷で正確なパイプラインパスを選択します。
ステップ 6: パフォーマンスと正確性を併せて検証する
変更の前後で同じ入力を再生します。インタラクションあたりのLayoutイベントの数と合計時間、ハンドラーに関連付けられた強制リフローの警告、ロングフレーム、スクリプト時間、ペイント領域、ドロップされたフレームを比較します。他のエンジニアが再現できるように、トレースの設定を報告します。
カードの境界、折り返し、フォーカス順序、ポインタターゲット、スクロール位置、ズーム、動的なフォントや画像、複数のビューポートサイズを検証します。初期レンダリング後の急激なイベントバーストやコンテンツの変更をテストします。カードが重なったりキーボードフォーカスが飛んだりする場合、レイアウトが減少したトレースであっても合格とは言えません。
普遍的な数値の約束は避けてください。デバイスのリフレッシュレートやワークロードは異なり、単一のローカルなフレームレート値はプロダクション環境の保証にはなりません。リリースの基準は、同じワークロードに対して繰り返される強制レイアウトが大幅に削減されていること、新たな支配的ボトルネックが存在しないこと、そして代表的なデバイス上でユーザーに見える動作が維持されていることです。
質の高い模範解答
「同じ300枚のカードを使用して1つの固定されたリサイズシーケンスを再現し、Performanceパネルで記録します。繰り返されるLayoutイベントを選択し、そのイニシエータを追跡して、ブラウザが変更を統合する前にハンドラーがジオメトリを書き込み、その後 getBoundingClientRect() または offsetHeight を呼び出していることを確認します。これこそがレイアウトスラッシングと呼ぶべき因果関係のパターンです。紫色のカテゴリが存在するだけでは不十分です。
次に、更新前のレイアウトからすべてのカードを計算できるかどうかを確認します。可能な場合は、まずすべてのボックスを読み取り、プレーンなデータで次のスタイルを計算し、書き込みをまとめて実行します。リサイズおよびスクロールの通知を1つの保留中視覚更新にまとめます。requestAnimationFrame はその更新の配置に役立ちますが、それ単体で交互の読み書きを修復するわけではありません。測定が通常のフローを補正しているだけの場合は、CSS Gridを優先します。レンダリング後にサイズが個別に変化する場合は、ResizeObserver を検討し、フィードバックループを防止します。
カードBがカードAの書き込み後のサイズに真に依存している場合、古い値をキャッシュして修正完了とは見なしません。累積モデルから位置を導出するか、レイアウトエンジンに依存関係を任せます。ドキュメントフローに影響を与える必要のない移動に対してのみ transform を使用します。
最後に、同じ記録を繰り返して、レイアウト回数と所要時間、強制リフローのスタック、フレームのギャップ、スクリプト実行、ペイントを比較します。また、境界、折り返し、フォーカス、スクロールアンカリング、ズーム、遅延ロードされるコンテンツも検証します。成功の定義は、古い測定値の発生やペイントにおける新たなボトルネックを伴うことなく、繰り返される強制レイアウトが消失または大幅に減少することです。」
よくある間違い
- すべてのレイアウトイベントをスラッシングと呼ぶ → ジオメトリが変化する際は常にレイアウトが必要 → 交互の依存関係によって引き起こされる繰り返しの同期フラッシュを示す。
- 元のループを
requestAnimationFrameで囲むだけにする → 1つのコールバック内で同じ読み取りと書き込みが依然として交互に行われる → まず読み取り、計算、書き込みのフェーズを分離する。 - すべての測定値を永久にキャッシュする → フォント、コンテンツ、ズーム、ビューポートの変更によってジオメトリが古くなる → 無効化のトリガーを定義するか、適切なオブザーバーを使用する。
- 解決策として
will-changeを使用する → レイヤーのヒントはジオメトリの依存関係を排除せずリソースを消費する → データフローを修正し、正当な理由のある視覚効果のみをレイヤー昇格させる。 - width を盲目的に transform scale に置き換える → ドキュメントフロー、テキスト、ヒットテスト、視覚的品質に不具合が生じる可能性がある → セマンティクスが許容する場合にのみコンポジターに適した変更を使用する。
- トレースなしで最適化する → 高負荷な処理は他の場所でのスクリプトやペイントである可能性がある → インタラクションをキャプチャし、イベントのイニシエータをコードまで追跡する。
- 平均FPSのみを確認する → 平均値はロングフレームを隠蔽し原因を特定できない → 同じワークロードに対してレイアウト回数、所要時間、スタック、フレームギャップを比較する。
- 視覚的な正確性を無視する → 古いジオメトリによってトレース上は速く見える可能性がある → リファクタリング後にレイアウト、フォーカス、スクロール、ズーム、非同期コンテンツをテストする。
フォローアップの質問と回答
フォローアップ 1: requestAnimationFrame は強制同期レイアウトを防ぎますか?
いいえ。将来のペイントの前にコールバックをスケジュールするだけです。そのコールバックがジオメトリの書き込みとレイアウト依存の読み取りを交互に行う場合、依然として強制同期レイアウトを繰り返し引き起こし、フレームを遅延させる可能性があります。依存関係の順序を修正した後に、バッチ書き込みやアニメーションステップを調整するために使用してください。
フォローアップ 2: 1回の強制レイアウトが許容されるのはどのような場合ですか?
現在のジオメトリが真に必要とされ、処理が限定的である場合です(例:新しく開いたポップオーバーを配置する前に1回測定するなど)。必要な値をまとめて読み取り、ループ内でのフラッシュの繰り返しを避け、代表的なデバイスで負荷を検証してください。「強制(Forced)」はスケジューリングの状態を表すものであり、自動的にバグを意味するわけではありません。
フォローアップ 3: ResizeObserver はレイアウト処理を排除しますか?
いいえ。サイズが変更されたことを認識するために、ブラウザは依然としてレイアウトを実行します。オブザーバーは手動のポーリングを排除し、定義された段階で監視結果を提供するため、アプリケーションのデータフローを改善できます。ただし、そのコールバックが新しい監視をトリガーするサイズを繰り返し書き込むと、フィードバックループが作成される可能性があります。
フォローアップ 4: レイアウト回数は減少したものの、アニメーションが依然として遅い場合はどうしますか?
新しいトレースを比較します。スクリプト実行が支配的である、ペイント領域が大きい、インタラクション中に画像デコードが発生している、または合成レイヤーが多すぎてリソースを消費している可能性があります。レイアウトがフレームギャップの原因でなくなった後は、レイアウトの削減を続けるのではなく、新しく測定されたボトルネックを最適化してください。
フォローアップ 5: コンポーネントフレームワークでこれをどのようにテストしますか?
トレース内でフレームワークのコミットと測定エフェクトをマークし、フレームワークのDOM書き込みの後にアプリケーションコードが読み取りを行っているかどうかを判断します。正確性を確保するために必要なライフサイクルフェーズ内で測定を行い、処理を限定してフェーズを分離します。プロダクション環境のコストをフレームワーク自体の責任にする前に、繰り返されるマウント、状態更新、遅延コンテンツ、開発モードのアーティファクトをテストしてください。