ResizeObserver: 要素の document.onresize に似ています。

ResizeObserver を使用すると、要素のサイズが変更されたときに通知を受け取ることができます。

ResizeObserver より前は、ビューポートのサイズの変更を通知するには、ドキュメントの resize イベントにリスナーをアタッチする必要がありました。イベント ハンドラでは、その変更の影響を受けた要素を特定し、適切な処理を行うための特定のルーティンを呼び出す必要があります。サイズ変更後の要素の新しいディメンションが必要な場合は、getBoundingClientRect() または getComputedStyle() を呼び出す必要がありました。読み取りと書き込みのすべてをバッチ処理しないと、レイアウト スラッシングが発生する可能性があります。

メイン ウィンドウのサイズが変更されずに要素のサイズが変更されるケースは、この方法ではカバーできませんでした。たとえば、新しい子を追加したり、要素の display スタイルを none に設定したりするなどのアクションを行うと、要素、兄弟、祖先のサイズが変更される可能性があります。

これが ResizeObserver が便利なプリミティブである理由です。変更の原因に関係なく、監視対象の要素のサイズの変更に反応します。また、監視対象要素の新しいサイズにもアクセスできます。

Browser Support

  • Chrome: 64.
  • Edge: 79.
  • Firefox: 69.
  • Safari: 13.1.

Source

API

上記で説明した Observer 接尾辞が付いたすべての API は、シンプルな API 設計を共有しています。ResizeObserver も例外ではありません。ResizeObserver オブジェクトを作成し、コールバックをコンストラクタに渡します。コールバックには、観測された要素ごとに 1 つのエントリを含む ResizeObserverEntry オブジェクトの配列が渡されます。この配列には、要素の新しいディメンションが含まれています。

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);

詳細

報告の内容

通常、ResizeObserverEntrycontentRect というプロパティを介して要素のコンテンツ ボックスをレポートします。このプロパティは DOMRectReadOnly オブジェクトを返します。コンテンツ ボックスは、コンテンツを配置できるボックスです。これは、パディングを除いたボーダーボックスです。

CSS ボックスモデルの図。

ResizeObservercontentRect とパディングの両方のディメンションをレポートしますが、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)'));

もう 1 つ興味深い例は、チャット ウィンドウです。一般的な上から下への会話レイアウトで発生する問題は、スクロール位置です。ユーザーを混乱させないように、ウィンドウは会話の一番下(最新のメッセージが表示される場所)に固定しておくと便利です。また、レイアウトの変更(スマートフォンが横向きから縦向きに、またはその逆に変わるなど)でも同じ結果が得られるようにする必要があります。

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 を使用してレンダリングを遅延させすぎないように注意してください。