プロンプトとスコープ
マルチカラム構成のダッシュボードを保守しているとします。カードのサイズはグリッド、ドラッグ、サイドバー、フォントの読み込みによって決定されるため、ビューポートサイズだけの関数として扱うことはできません。320px未満ではコンパクトなタイトルと1列のメトリクスを表示し、その閾値を超えると完全なレイアウトを表示します。ウィンドウサイズを変更せずにサイドバーを折りたたんだ場合でも、即座に反映されなければなりません。
この設問では、要素レベルの測定、ブラウザのレイアウトタイミング、React のライフサイクル、そしてパフォーマンス境界をテストします。一般的なフロントエンドの面接資料では DOM、CSS、ブラウザイベント、パフォーマンスが広く扱われていますが、プラットフォームのドキュメントには、より踏み込んだ質問に対応するための正確な ResizeObserver の通知およびループの挙動が記載されています。
面接官がテストしていること
window.resizeはビューポートの変更を表すものであり、グリッドのリフロー、フォントの差し替え、親要素のサイズ変更を表すものではないことを説明できるか。content-boxまたはborder-boxを選択し、その閾値が何を意味するかを定義できるか。- 監視対象の要素を同期的にリサイズすることなく、測定値を状態や CSS 変数に書き込めるか。
- ターゲットの置換、アンマウント、非表示ノード、古いブラウザ、多数のカードの処理に対応できるか。
- 単なるコールバックの実行回数ではなく、ユーザーメトリクス、ロングタスク(long tasks)、レイアウトトレースを用いて滑らかさを証明できるか。
最初に確認すべき明確化のための質問
- その閾値はコンテンツ幅(content-box)ですか、ボーダーボックス幅(border-box)ですか、それとも CSS コンテナクエリで表現可能なスタイルのみの決定事項ですか?
- サイズの変更によってデータ処理をトリガーする必要がありますか、それとも表示のみを変更しますか?
- UI は毎フレーム追従する必要がありますか、それとも1フレームあたり1回にまとめられた(coalesced)更新で十分ですか?
- ブラウザのサポート基準は何ですか、またサーバーサイドレンダリングが
windowに触れる可能性はありますか? - カードは数十枚ですか、それとも数千枚ありますか。また、可視状態のカードのみを監視することは可能ですか?
30秒で答える要約
「まず、これがビューポートサイズの問題ではなく、要素サイズの問題であることを明確にします。スタイルのみのブレークポイントであれば、CSS コンテナクエリを優先します。JavaScript で測定値が必要な場合は、クライアント上で ResizeObserver をアタッチし、定義されたボックスを読み取り、閾値状態の重複を排除して、CSS 変数または離散的な状態に書き込みます。コールバック内で監視対象のサイズを同期的に変更すべきではありません。必須でない書き込みは次のフレームにスケジュールし、クリーンアップでオブザーバーを切断(disconnect)する必要があります。カードが多数ある場合は、監視対象のセットを制限し、INP、ロングタスク、レイアウトコストを測定します。古いブラウザには、固定レイアウト、コンテナクエリ、またはスロットルされたビューポートリスナーによる明示的なフォールバックを用意します。」
詳細解説
ステップ 1: CSS だけで十分か確認する
要件が「320px 未満でコンパクトなスタイルを表示する」ことだけであれば、通常はコンテナクエリの方がシンプルであり、測定処理を JavaScript の外に保つことができます。サイズがチャートのサンプリング、仮想化、サードパーティ製レンダラー、または監視可能なビジネスロジックを駆動する場合に ResizeObserver の使用が正当化されます。
ステップ 2: サイズの契約を定義する
ResizeObserverEntry は content-box と border-box の測定値を公開します。どちらのボックスが閾値を担うかを定義し、単位と丸め処理を標準化します。浮動小数点数のサンプルは必ずしもビジネスイベントとは限りません。プロダクトの契約がそうであるなら、319.9から320.1への遷移はブレークポイント状態として重複排除(deduplicate)します。
ステップ 3: オブザーバーのライフサイクルを確立する
クライアントコンポーネントがマウントされた後にオブザーバーを作成し、カードのノードを監視し、ノードの変更時やコンポーネントのアンマウント時に unobserve または disconnect を呼び出します。React では、通常のレンダリングでオブザーバーが再生成されないよう、ref にターゲットを保持させ、Effect にオブザーバーの作成とクリーンアップを担当させます。
const ref = useRef<HTMLDivElement>(null)
const [compact, setCompact] = useState(false)
useEffect(() => {
const node = ref.current
if (!node || !('ResizeObserver' in window)) return
const observer = new ResizeObserver(([entry]) => {
const width = entry.contentRect.width
const next = width < 320
setCompact((current) => (current === next ? current : next))
})
observer.observe(node)
return () => observer.disconnect()
}, [])ステップ 4: コールバックのフィードバックループを防ぐ
コールバックが監視対象要素の幅や高さを変更すると、その変更によって別の通知がスケジュールされ、最終的に ResizeObserver loop completed with undelivered notifications が発生する可能性があります。コールバックはサイズ契約の処理に集中させるか、視覚的な書き込みを requestAnimationFrame でスケジュールして冪等(idempotent)にします。
ステップ 5: 測定とレンダリングを分離する
CSS がレイアウトを制御できる場合は測定値を CSS カスタムプロパティに書き込むか、compact のような離散的な状態のみを保持します。ドラッグ中に React の状態へすべてのピクセル変化を反映させてはいけません。連続した値が必要な場合は、アニメーションフレームごとに通知を統合(coalesce)し、ドロップフレームやロングタスクを記録します。
ステップ 6: 多数のカードと非表示ノードを処理する
数千枚のカードがある場合は、可視ノードのみを監視するか、レイアウト層から測定値を分配させます。display: none、折りたたまれたパネル、仮想化によって監視可能なサイズが変化します。ノードが可視化された後、最初の通知で正しい状態が復元されることを検証します。オブザーバーはポーリングループではありません。
ステップ 7: フォールバックとサーバーの境界を定義する
サーバーレンダリングは window にアクセスできません。クライアントの Effect 内でサポート状況を検出します。ResizeObserver が利用できない場合は、固定レイアウト、CSS メディア/コンテナルール、またはスロットルされたビューポートリスナーを使用し、フォールバックでは提供できない要素レベルの挙動をドキュメント化します。
ステップ 8: エビデンスをもって検証する
サイドバーの折りたたみ、ドラッグ、フォントの読み込み、グリッドのリフロー、画面の回転、ブラウザのズームをテストします。Chrome の Performance パネルでコールバック、レイアウト、ペイント、ロングタスクを記録し、INP や入力遅延(input delay)を使用してドラッグの応答性を確認します。コンソールのループ警告に関するテストを追加し、アンマウントされたカードが更新を受信しなくなることを検証します。
トレードオフと境界
コンテナクエリ vs ResizeObserver
コンテナクエリはスタイルのみのブレークポイントに適しており、宣言的な状態を維持できます。ResizeObserver は JavaScript による計算やサードパーティのレンダリングに適していますが、ライフサイクルとパフォーマンスの制御が必要です。その境界は、測定値がスタイル層を出る必要があるかどうかです。
content-box vs border-box
コンテンツレイアウトの閾値には content-box を使用し、パディングとボーダーを含むカード外側の契約には border-box を使用します。選択を誤るとブレークポイントがずれるため、契約とテストにその選択を明記します。
連続値 vs 離散値
連続的な幅の値はチャートを駆動できますが、頻繁に更新されます。離散的なブレークポイントは安定しており、テストが容易です。まずは離散的な契約から始め、測定された予算と明確な視覚的メリットがある場合にのみ連続更新を追加します。
障害訓練と発展
障害:window.resize のみをリッスンする
サイドバーの折りたたみやグリッドのリフローではビューポートが変化しないため、カードが誤ったレイアウトのままになります。カードまたはそのコンテナを監視し、ビューポート以外の変更をテストします。
障害:コールバック内で監視対象のサイズを変更する
サイズ変更が別のコールバックをトリガーし、ループや余分なレイアウトを発生させる可能性があります。独立した CSS 変数に書き込むか、離散的な状態を使用するか、次のフレームへの冪等な更新をスケジュールします。
障害:1ピクセルごとに setState を実行する
ドラッグによって高頻度な React レンダリングが発生し、入力応答性が悪化します。閾値を重複排除し、rAF で統合し、可視カードのみを監視して、Performance パネルでコストを確認します。
よくある間違いとフォローアップ
間違い:ResizeObserver をより強力な resize イベントとして扱うこと
要素のボックスを監視し、レイアウトタイミングに従って通知を配信するものであり、単なるビューポートイベントの代替品ではありません。
フォローアップ:React Strict Mode をどのように検証しますか?
開発環境でのセットアップとクリーンアップが正しくペアになっており、オブザーバーの数が増加し続けないことを確認します。開発時の追加チェックを本番環境での重複購読と混同しないでください。
フォローアップ:コールバックから getBoundingClientRect を呼び出すことはできますか?
可能ですが、追加の測定によってレイアウトコストが増加する可能性があります。エントリのボックス値を優先し、追加の読み取りを行う場合はパフォーマンストレースで妥当性を証明してください。
フォローアップ:サイズループをどのようにテストしますか?
コールバックで自身のサイズに影響を与えるスタイルを変更させ、警告、通知回数、最終的なサイズを観察した上で、修正版が継続的な通知なしに安定することを確認(assert)します。
フォローアップ:JavaScript が不要なのはどのような場合ですか?
要件がコンテナのブレークポイントによるスタイルの変更のみである場合は、CSS コンテナクエリを優先します。JavaScript は、それを真に必要とするデータ処理、測定、またはサードパーティレンダリングのために残しておきます。
フォローアップ:最適化が機能したことをどのように証明しますか?
同一のドラッグスクリプトのもとで、INP、ロングタスク数、レイアウト時間、React のコミット、メモリを比較します。コールバックの実行回数だけではユーザー体験のメトリクスにはなりません。