ResizeObserver
は、要素のサイズが変更されたときに通知します。
ResizeObserver
より前は、ビューポートのサイズの変更を通知するには、ドキュメントの resize
イベントにリスナーをアタッチする必要がありました。イベント ハンドラでは、その変更の影響を受けた要素を特定し、適切に対応するために特定のルーティンを呼び出す必要があります。サイズ変更後に要素の新しいディメンションが必要な場合は、getBoundingClientRect()
または getComputedStyle()
を呼び出す必要がありました。読み取りと書き込みのすべてをバッチ処理しないと、レイアウト スラッシングが発生する可能性があります。すべて
メイン ウィンドウのサイズ変更なしで要素のサイズが変更される場合も考慮されていませんでした。たとえば、新しい子を追加したり、要素の display
スタイルを none
に設定したりするなどの操作を行うと、要素、その兄弟、その祖先のサイズが変更される可能性があります。
これが ResizeObserver
が便利なプリミティブである理由です。変更の原因に関係なく、監視対象の要素のサイズの変更に反応します。また、監視対象要素の新しいサイズにもアクセスできます。
API
上記の Observer
接尾辞を持つすべての API は、単純な API 設計を共有しています。ResizeObserver
も例外ではありません。ResizeObserver
オブジェクトを作成し、コールバックをコンストラクタに渡します。コールバックには、要素の新しいディメンションを含む ResizeObserverEntry
オブジェクトの配列(観測された要素ごとに 1 つのエントリ)が渡されます。
var ro = new ResizeObserver(entries => {
for (let entry of entries) {
const cr = entry.contentRect;
console.log('Element:', entry.target);
console.log(`Element size: ${cr.width}px x ${cr.height}px`);
console.log(`Element padding: ${cr.top}px ; ${cr.left}px`);
}
});
// Observe one or multiple elements
ro.observe(someElement);
詳細
報告されている内容
通常、ResizeObserverEntry
は contentRect
というプロパティを介して要素のコンテンツ ボックスを報告します。このプロパティは DOMRectReadOnly
オブジェクトを返します。コンテンツ ボックスは、コンテンツを配置できるボックスです。境界ボックスからパディングを差し引いたものです。
ResizeObserver
は contentRect
のディメンションとパディングの両方をレポートしますが、contentRect
のみを監視します。contentRect
を要素の境界ボックスと混同しないでください。getBoundingClientRect()
によって報告される境界ボックスは、要素全体とその子孫を含むボックスです。SVG は例外で、ResizeObserver
は境界ボックスのサイズを報告します。
Chrome 84 以降では、ResizeObserverEntry
に 3 つの新しいプロパティが追加され、より詳細な情報を提供できるようになりました。これらのプロパティはそれぞれ、blockSize
プロパティと inlineSize
プロパティを含む ResizeObserverSize
オブジェクトを返します。この情報は、コールバックが呼び出された時点でオブザーバーされた要素に関するものです。
borderBoxSize
contentBoxSize
devicePixelContentBoxSize
これらのアイテムはすべて読み取り専用の配列を返します。これは、将来的に、複数の列のシナリオで発生する複数のフラグメントを持つ要素をサポートできるようになることを期待しているためです。現時点では、これらの配列には 1 つの要素のみが含まれます。
これらのプロパティのプラットフォーム サポートは限定的ですが、最初の 2 つは Firefox ですでにサポートされています。
いつ報告されましたか?
仕様では、ResizeObserver
はペイントの前に、およびレイアウトの後にすべてのサイズ変更イベントを処理することが規定されています。そのため、ResizeObserver
のコールバックは、ページのレイアウトを変更するのに最適な場所です。ResizeObserver
の処理はレイアウトとペイントの間に行われるため、レイアウトのみが無効になり、ペイントは無効になりません。
Gotcha
コールバック内で監視対象要素のサイズを ResizeObserver
に変更するとどうなりますか?答えは、コールバックへの別の呼び出しがすぐにトリガーされることです。幸い、ResizeObserver
には、無限のコールバック ループと循環依存関係を回避するメカニズムがあります。変更が同じフレームで処理されるのは、サイズ変更された要素が、前のコールバックで処理された最も浅い要素よりも DOM ツリー内で深い場合のみです。処理されなかった場合は、次のフレームに延期されます。
アプリケーション
ResizeObserver
を使用すると、要素ごとのメディアクエリを実装できます。要素を監視することで、設計のブレークポイントを強制的に定義し、要素のスタイルを変更できます。次の例では、2 番目のボックスの境界半径は幅に応じて変更されます。
const ro = new ResizeObserver(entries => {
for (let entry of entries) {
entry.target.style.borderRadius =
Math.max(0, 250 - entry.contentRect.width) + 'px';
}
});
// Only observe the second box
ro.observe(document.querySelector('.box:nth-child(2)'));
チャット ウィンドウも興味深い例です。一般的な上から下への会話レイアウトで発生する問題は、スクロールの位置です。ユーザーが混乱しないようにするには、ウィンドウを会話の一番下に固定し、最新のメッセージが表示されるようにします。また、どのようなレイアウト変更でも(スマートフォンを横向きから縦向きに切り替える、またはその逆)、同じ結果が得られるようにする必要があります。
ResizeObserver
を使用すると、両方のシナリオに対応する単一のコードを書くことができます。ウィンドウのサイズ変更は、定義上 ResizeObserver
がキャプチャできるイベントですが、appendChild()
を呼び出すと、新しい要素のためのスペースを確保するために、その要素のサイズも変更されます(overflow: hidden
が設定されていない場合)。これを念頭に置いて、目的の効果を実現するために必要な行数はごくわずかです。
const ro = new ResizeObserver(entries => {
document.scrollingElement.scrollTop =
document.scrollingElement.scrollHeight;
});
// Observe the scrollingElement for when the window gets resized
ro.observe(document.scrollingElement);
// Observe the timeline to process new messages
ro.observe(timeline);
便利だと思いませんか?
ここから、ユーザーが手動でスクロールして、新しいメッセージが届いたときにそのメッセージにスクロールを固定したい場合を処理するコードを追加できます。
別のユースケースは、独自のレイアウトを実行するあらゆる種類のカスタム要素です。ResizeObserver
までは、子要素を再レイアウトできるように、サイズが変更されたときに通知を受け取る信頼できる方法がありませんでした。
Interaction to Next Paint(INP)への影響
Interaction to Next Paint(INP)は、ユーザー インタラクションに対するページの全体的な応答性を測定する指標です。ページの INP が「良好」のしきい値(200 ミリ秒以下)内にある場合、そのページはユーザーの操作に対して確実にレスポンスしていると判断できます。
ユーザー操作に応じてイベント コールバックが実行されるまでの時間は、インタラクションの合計レイテンシに大きく影響しますが、INP で考慮すべき要素は他にもあります。INP では、インタラクションの次のペイントが発生するまでの時間も考慮されます。これは、インタラクションに応じてユーザー インターフェースを更新するために必要なレンダリング作業が完了するまでの時間です。
ResizeObserver
の場合、これは重要です。ResizerObserver
インスタンスが実行するコールバックはレンダリング処理の直前に発生するためです。これは設計上のものです。コールバックで発生する処理は考慮する必要があります。その処理の結果、ユーザー インターフェースの変更が必要になる可能性が非常に高いためです。
レンダリング作業を過度に行うと、ブラウザが重要な作業を遅らせる状況が発生する可能性があるため、ResizeObserver
コールバックで必要なレンダリング作業を最小限に抑えるようにしてください。たとえば、ResizeObserver
コールバックを実行するコールバックがインタラクションにある場合は、可能な限りスムーズなエクスペリエンスを実現するために、次の操作を必ず行ってください。
- スタイルの再計算の過剰な作業を回避するために、CSS セレクターをできるだけシンプルにします。スタイルの再計算はレイアウトの直前に行われ、複雑な CSS セレクタはレイアウト オペレーションを遅らせる可能性があります。
- 強制再フローをトリガーする可能性がある処理を
ResizeObserver
コールバックで実行しないでください。 - 通常、ページのレイアウトを更新するのにかかる時間は、ページの DOM 要素の数に比例して増加します。これは、ページが
ResizeObserver
を使用するかどうかに関係なく当てはまりますが、ページの構造の複雑さが増すにつれて、ResizeObserver
コールバックで実行される処理は大幅に増加する可能性があります。
まとめ
ResizeObserver
はすべての主要ブラウザで使用でき、要素レベルで要素のサイズ変更を効率的にモニタリングできます。この強力な API でレンダリングが遅れすぎないように注意してください。