代表的な面接トピック

フロントエンド面接:レイアウトスラッシングを起こさずにResizeObserverを使用する

フロントエンド普通
Offer.cc 編集チーム公開日 更新日

質問

リサイズ可能なダッシュボードカードがコンテナの幅に基づいてレイアウトを切り替えます。レイアウトスラッシング、フィードバックループ、重複購読、アンマウント後の更新を起こさずにResizeObserverを使用するにはどうすればよいですか?

プロンプトとスコープ

ダッシュボードカードが、ビューポートではなく自身のコンテナに基づいてレイアウトを変更します。最初の実装では、ResizeObserverのコールバック内で同期的にスタイルの読み取りと書き込みを行っているため、フレームの遅延やループエラーが時折発生しています。監視、測定、更新、クリーンアップを設計してください。コアスキルはブラウザのレイアウトとコンポーネントのライフサイクルであるため、これはフロントエンドに属します。

面接官が評価するポイント

回答では、ビューポートのメディアクエリと要素の監視を明確に区別し、コールバックのタイミング、ボックスの選択肢、読み取り/書き込みの分離、ループ保護、バッチ更新、React Strict Mode、複数ターゲット、非表示要素、SSR、アクセシビリティを網羅する必要があります。

最初に確認すべき明確化のための質問

  • 監視対象はcontent box、border box、それともdevice-pixel content boxですか?
  • 更新によってクラス、CSS変数、Canvasの寸法、またはDOMジオメトリのいずれが変更されますか?
  • コールバックは監視対象の要素またはその祖先要素のサイズを変更しますか?
  • ターゲットはいくつ存在し、更新は次のフレームにまとめる(coalesce)べきですか?
  • アンマウント時、非表示状態、SSRにおけるオブザーバーの作成とクリーンアップはどのように処理されますか?

30秒の回答フレームワーク

「クライアント上で安定したオブザーバーを1つ作成し、必要なボックスを監視します。コールバック内では、まずサイズを収集し、DOMの読み取りと書き込みを分離します。レイアウトの変更が即座に監視対象へフィードバックしないよう、バッチ処理されたフレームでクラスまたはCSS変数を適用します。変更のない値は無視し、アンマウント時には更新を停止し、unobserveおよびdisconnectを行います。ロングタスク、レイアウト回数、ループエラー、StrictModeの再マウントをテストします。」

ステップバイステップの解決策

ResizeObserverは、ビューポートとは無関係に要素のcontent boxまたはborder boxを監視します。ボックスは慎重に選択してください。レイアウトのブレークポイントには通常contentまたはborderサイズを使用しますが、ピクセル精度が求められる描画にはdevice-pixel contentを使用することがあります。コールバック内でレイアウトを強制するプロパティを繰り返し読み取らないでください。

まず各エントリの最新サイズを小さな保留中セットに収集し、その後ブレークポイントをまとめて計算します。ジオメトリの読み取りを1つのバッチにまとめ、計算後にCSS変数またはクラスを書き込みます。書き込みがレイアウトに影響を与える可能性がある場合は、requestAnimationFrameでスケジュールし、次のバッチを前回の値と比較します。

フィードバックループとは、「サイズを監視し、スタイルを書き込み、サイズが再び変化する」ことです。例えば、幅がパディングを変更し、パディングが幅を変更するようなケースです。ヒステリシスや固定コンテナの制約を追加するか、測定対象を変更しないプロパティを更新してください。サイズ変更が必要な場合は、処理を1フレームあたり1回の更新に制限し、収束を検出します。ブラウザは収束しない通知を遅延させてループエラーを報告することがあります。エラーを無視することは解決策になりません。

Reactでは、安定したrefとコールバックを使用して、クライアント専用のuseLayoutEffectまたは適切なエフェクト内でオブザーバーを作成します。Strict Modeではセットアップとクリーンアップが2回実行される可能性があるため、クリーンアップは冪等(idempotent)でなければなりません。アンマウント時には更新の適用を停止し、ターゲットのunobserveとdisconnectを行います。SSRではwindowへのアクセスやオブザーバーの構築を行ってはなりません。

カードが多数ある場合は、オブザーバーを共有してentry.targetをコンポーネントの状態にマッピングしますが、1つのカードがコールバック全体をブロックしないように所有権を明確に保ちます。ドラッグ中は最新のサイズのみを保持し、レイアウト遅延の原因となる恣意的なdebounceを追加するのではなく、アニメーションフレーム内で更新をマージします。

非表示要素、display:none、フォントの読み込み、スクロールバーによってサイズが変わることがあります。最初のゼロサイズのエントリに対して使用可能なデフォルト値を提供し、最初の有効な測定の後に切り替えます。純粋なスタイルのブレークポイントにはCSSコンテナクエリを優先し、JavaScriptでCanvas、サードパーティ製ウィジェット、またはビジネス計算を制御する必要がある場合にResizeObserverを使用します。

ドラッグ中、フォント読み込み中、カードの非表示・表示時、ビューポートの回転時、および多数のインスタンスのマウント時に、Performanceパネルで検証します。各サイズ変更が必要な処理のみをトリガーし、持続的なループ、重複するオブザーバー、アンマウント後の更新、重大なレイアウトスラッシングが発生しないことを確認します。キーボードやスクリーンリーダーの操作もテストします。

質の高い模範解答

「私はクライアント側で安定したResizeObserverを作成し、content boxまたはborder boxを意図的に選択します。コールバックは最新のエントリを記録してブレークポイントを計算し、レイアウトの読み取りと書き込みを繰り返し実行しません。バッチ処理されたフレームでCSS変数またはクラスを適用します。書き込みによって監視対象のサイズが変化する場合は、ヒステリシス、制約、収束チェックを追加してフィードバックループを防ぎます。

Reactのクリーンアップは冪等にします。アンマウント時に更新を停止し、unobserveとdisconnectを実行します。複数のカードは、ターゲットベースのディスパッチを用いて1つのオブザーバーを共有できます。ドラッグリサイズ、フォント読み込み、非表示・表示状態、Strict Modeの再マウントをテストし、レイアウト、ロングタスク、ループエラー、重複購読、アクセシブルなインタラクションを測定します。」

よくある間違い

  • ResizeObserverをビューポートメディアクエリとして扱う → 異なるコンテナでコンポーネントが壊れる → 要素自体を監視する。
  • コールバック内で大量のDOMを読み書きする → 強制レイアウトとスラッシングが発生する → 読み取りをバッチ化し、次のフレームで書き込む。
  • 収束させずに監視対象を変更する → フィードバックループが発生する → ヒステリシス、制約、検出を使用する。
  • レンダーごとにオブザーバーを作成する → コールバックの重複とリークが発生する → 安定したref、依存配列、クリーンアップを使用する。
  • unobserveのみを行いコールバックの状態を保持する → アンマウント後も更新が継続する → 非アクティブとマークし、unobserveおよびdisconnectを行う。
  • ゼロサイズに最大のレイアウトを適用する → 初回描画時に表示が跳ねる → 有効なエントリが得られるまでデフォルト状態を使用する。
  • すべてのブレークポイントにJavaScriptを使用する → 無駄な複雑さが増す → CSSで十分な場合はコンテナクエリを使用する。
  • 通常のドラッグのみをテストする → フォント、非表示状態、Strict Modeの問題を見落とす → ライフサイクルとリソースの変更を網羅する。

フォローアップの質問と回答

フォローアップ1:コールバックはいつ実行されますか?

ブラウザはレイアウト後にサイズ変更通知をバッチ処理します。収束しない変更は遅延され、ループエラーが発生する可能性があるため、コールバックが無制限のレイアウト変更を引き起こしてはなりません。

フォローアップ2:なぜwindow.resizeを使用しないのですか?

これはビューポートの変更を反映するものであり、グリッド、サイドバー、フォントによるカード自体の変化を反映しないためです。ResizeObserverは要素を直接監視します。

フォローアップ3:読み取りと書き込みをどのように分離しますか?

最初にエントリからサイズを読み取って結果を計算し、必要に応じてrequestAnimationFrameを使用してCSS変数またはクラスの書き込みをバッチ処理し、変更のない値は無視します。

フォローアップ4:要素ごとに1つのオブザーバーを作成すべきか、それとも共有オブザーバーにすべきですか?

独立して管理される少数の要素であれば、個別のオブザーバーを使用できます。ターゲットが多数ある場合は、コールバックの処理が肥大化しない限り、1つを共有してターゲットごとにディスパッチできます。

フォローアップ5:display:noneの後はどうなりますか?

ゼロサイズを一時的なものとして扱い、表示された後の有効なエントリを待ち、非表示中に書き込みループが発生するのを防ぎます。ゼロを恒久的なブレークポイントとして扱わないでください。

フォローアップ6:なぜReact Strict Modeでバグが浮き彫りになるのですか?

開発モードでは、非対称な副作用を明らかにするためにエフェクトのセットアップとクリーンアップが2回実行されることがあります。クリーンアップは再現可能でなければならず、オブザーバーや更新コールバックが残らないようにする必要があります。

フォローアップ7:どのような場合にCSSコンテナクエリを使用すべきですか?

純粋なコンテナブレークポイントにはCSSを使用します。サイズによってCanvas、サードパーティAPI、複雑な測定、またはビジネス計算を制御する必要がある場合はResizeObserverを使用します。

公開情報ソース

関連する質問