1. ホーム
  2. JavaScript

【JavaScript】ResizeObserver の使い方|要素のサイズ変化を検知する方法

Share

「この要素の幅が狭くなったらレイアウトを縦積みにしたい」「グラフの描画サイズを親要素に合わせて作り直したい」——こうした処理を windowresize イベントで書くと、ウィンドウは変わっていないのに要素だけ変わったケースを取りこぼします。ResizeObserver要素そのもののサイズ変化を監視できる API です。この記事では基本の書き方、コールバックで受け取れる情報、動くデモ、そして「ResizeObserver loop」の警告が出るときの対処までを解説します。

window の resize イベントでは足りない理由

要素のサイズが変わるきっかけは、ウィンドウのリサイズだけではありません。サイドバーを開閉した、テキストが増えて折り返しが変わった、画像の読み込みが終わった、CSS の resize でユーザーが引っ張った、フォントの読み込みで行数が変わった。どれも windowresize イベントは発火しないので、そちらだけを見ていると変化に気づけません。

かといって setIntervaloffsetWidth を定期的に読みに行くやり方は、無駄な処理が常時走るうえに反応も遅れます。ResizeObserverブラウザーがレイアウトを確定させたタイミングで、変化があった要素の分だけコールバックを呼んでくれるので、この2つの問題をどちらも避けられます。主要ブラウザーはすべて対応済みで、そのまま使って問題ありません。

基本の使い方

使い方は2ステップです。コールバック関数を渡して ResizeObserver のインスタンスを作り、監視したい要素を observe() に渡します。

main.js
const target = document.querySelector('.card');

// 1. コールバックを渡してインスタンスを作る
const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    console.log(entry.target, entry.contentRect.width);
  }
});

// 2. 監視を開始する
observer.observe(target);

コールバックの第1引数 entries配列です。ひとつのインスタンスで複数の要素を監視でき、同じタイミングで複数の要素が変化したときはまとめて渡されるため、常に for...of などで回して処理します。entries[0] だけを見る書き方は、監視対象が1つのうちは動きますが、増やしたときに壊れます。

もうひとつ覚えておきたいのが、observe() を呼んだ直後にもコールバックが1回実行されることです。「サイズが変わったとき」だけでなく「監視を始めたとき」の初期状態も届くので、初回の描画処理を別途書く必要がありません。

entry から取り出せるサイズ情報

コールバックに渡される各 entryResizeObserverEntry)には、変化した要素とそのサイズが入っています。

プロパティ内容
targetサイズが変わった要素そのもの
contentRectコンテンツ領域の矩形。width / height / top / left を持つ
contentBoxSizeコンテンツ領域のサイズ。inlineSize / blockSize を持つオブジェクトの配列
borderBoxSize枠線を含めたサイズ。配列である点は contentBoxSize と同じ
devicePixelContentBoxSizeデバイスピクセル単位のコンテンツ領域サイズ。canvas の描画に使う

いちばん手軽なのは contentRect.width です。paddingborder を含まない、中身の領域の幅が取れます。枠線や余白まで含めた実際の占有幅がほしいときは borderBoxSize[0].inlineSize を使います。

inlineSizeblockSize という名前に戸惑うかもしれませんが、これは書字方向に依存しない呼び方です。横書きの日本語や英語なら inlineSize が幅、blockSize が高さに対応します。縦書きでは入れ替わります。

配列になっているのは、段組みなどで要素が複数の領域に分かれる将来のケースを想定した仕様のためです。実際のところ現在のブラウザーでは要素が1つ入るだけなので、[0] を取れば問題ありません。

main.js
const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    // padding・border を含まない中身の幅
    const contentWidth = entry.contentRect.width;

    // border まで含めた幅(横書きの場合)
    const borderWidth = entry.borderBoxSize[0].inlineSize;

    console.log(contentWidth, borderWidth);
  }
});

要素の幅に応じてクラスを付け替える

実際に動かしてみましょう。監視対象のボックスに resize: both を指定してユーザーがドラッグできるようにし、幅と高さを表示しつつ、幅が 240px を下回ったら is-narrow クラスを付けて配色を切り替えます。プレビューの右下の角をドラッグしてみてください。

<div class="box" id="box">
  <p class="label" id="label">サイズを計測中…</p>
  <p class="hint">右下の角をドラッグしてサイズを変えてみてください</p>
</div>
.box {
  /* ブラウザーの機能でユーザーがリサイズできるようにする */
  resize: both;
  overflow: auto;
  width: 320px;
  min-width: 140px;
  min-height: 110px;
  padding: 16px;
  border: 2px solid #007bff;
  border-radius: 8px;
  font-family: sans-serif;
  background: #f1f8ff;
}

/* 幅が狭いときだけ JavaScript から付けられるクラス */
.box.is-narrow {
  border-color: #e8590c;
  background: #fff4e6;
}

.label {
  margin: 0 0 8px;
  font-weight: bold;
}

.hint {
  margin: 0;
  font-size: 13px;
  color: #666;
}
const box = document.getElementById('box');
const label = document.getElementById('label');

const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    // contentRect からコンテンツ領域の幅・高さを取得する
    const { width, height } = entry.contentRect;
    label.textContent = `幅 ${Math.round(width)}px / 高さ ${Math.round(height)}px`;

    // 幅が 240px 未満のときだけクラスを付ける
    entry.target.classList.toggle('is-narrow', width < 240);
  }
});

// 監視を開始する(開始直後に1回コールバックが呼ばれる)
observer.observe(box);
Preview

ポイントは entry.target を使っている点です。コールバックの外にある変数を参照するのではなく、渡ってきた entry から要素を取り出すようにしておくと、複数の要素を同じインスタンスで監視するようになってもコードを書き換えずに済みます。

なお、これと同じ「親の幅に応じた出し分け」は CSS のコンテナクエリ(@container)でも書けます。見た目を変えるだけなら CSS のほうが軽く、ResizeObserver の出番は数値そのものが必要なとき——canvas を描き直す、要素の数を計算して間引く、外部ライブラリーに新しいサイズを通知する、といったケースです。

observe の box オプションで基準を変える

observe() の第2引数に box を渡すと、どのボックスの変化を「サイズ変化」とみなすかを指定できます。既定は 'content-box' です。

main.js
// 枠線・余白を含めたサイズの変化を監視する
observer.observe(target, { box: 'border-box' });

// canvas をぼやけさせずに描き直したいとき
observer.observe(canvas, { box: 'device-pixel-content-box' });

'border-box' にすると、padding だけが変わったときにも通知が届きます。'device-pixel-content-box' は、ブラウザーのズームや高解像度ディスプレイを考慮した実ピクセル数で通知してくれるもので、canvas の width / height 属性を設定し直す用途に向いています。

注意したいのは、box を変えても entry から取れる情報は減らないことです。'border-box' で監視していても contentRect は読めます。box はあくまで「何が変わったらコールバックを呼ぶか」の条件だと考えてください。

監視をやめる(unobserve と disconnect)

監視の解除には2つのメソッドがあります。特定の要素だけ外すなら unobserve(target)、そのインスタンスの監視をすべて止めるなら disconnect() です。

main.js
// 特定の要素の監視だけをやめる
observer.unobserve(target);

// すべての監視をやめる(インスタンスは再利用できる)
observer.disconnect();

SPA や React・Vue のようにコンポーネントが消えたり現れたりする作りでは、後片付けを忘れると監視が残り続けます。React なら useEffect のクリーンアップ関数で disconnect() を呼ぶのが定石です。

useElementSize.js
import { useEffect, useRef, useState } from 'react';

export function useElementSize() {
  const ref = useRef(null);
  const [size, setSize] = useState({ width: 0, height: 0 });

  useEffect(() => {
    if (!ref.current) return;

    const observer = new ResizeObserver((entries) => {
      const { width, height } = entries[0].contentRect;
      setSize({ width, height });
    });
    observer.observe(ref.current);

    // アンマウント時に監視を止める
    return () => observer.disconnect();
  }, []);

  return [ref, size];
}

要素が DOM から削除されただけでは監視は自動的に解除されません。厳密にはガベージコレクションの対象にはなりますが、いつ回収されるかはブラウザー任せです。明示的に止めるほうが確実です。

「ResizeObserver loop」の警告が出るとき

コンソールに ResizeObserver loop completed with undelivered notifications.(ブラウザーによっては ResizeObserver loop limit exceeded)と出ることがあります。これは ResizeObserver を使った実装でいちばん遭遇しやすい問題です。

コールバックの中でサイズを変えている

原因はほぼこれです。コールバックの中で監視対象(またはそのレイアウトに影響する要素)のサイズを変えると、それが新しいサイズ変化としてまたコールバックを呼び、ブラウザーが1フレーム内で処理しきれずに警告を出します。

対処の基本はコールバックの中でレイアウトを変えないことです。上のデモのようにクラスを付け替えるだけなら、そのクラスが監視対象の幅に影響しないか(paddingborder-width を変えていないか)を確認します。どうしてもサイズを変える必要がある場合は、requestAnimationFrame で次のフレームに処理をずらすと連鎖が切れます。

main.js
const observer = new ResizeObserver((entries) => {
  // 次のフレームに回してループを断ち切る
  window.requestAnimationFrame(() => {
    for (const entry of entries) {
      entry.target.style.height = `${entry.contentRect.width * 0.5}px`;
    }
  });
});

要素が非表示だと通知が来ない

display: none の要素はレイアウトを持たないため、サイズは 0 として扱われ、変化も通知されません。タブ切り替えやアコーディオンの中身を監視している場合、開いた瞬間に 0 から実サイズへの変化として1回届きます。「開くまで値が 0 のまま」なのは仕様どおりの動作です。

同じ理由で、visibility: hidden の要素はレイアウトを持つのでサイズが取れる、という違いもあります。非表示の実装方法によって挙動が変わる点は覚えておくとよいでしょう。

コールバックが重くて描画が詰まる

ResizeObserver のコールバックはレイアウト確定後、描画の直前に実行されます。ここで重い処理を書くと、そのままフレーム落ちにつながります。ドラッグ中のように連続して呼ばれる場面では、実際の描画処理を requestAnimationFrame でまとめるか、確定的な処理だけを遅延させる仕組みを挟むのが安全です。

まとめ

ResizeObserver は、ウィンドウではなく要素単位のサイズ変化を検知する API です。new ResizeObserver(コールバック) でインスタンスを作り、observe(要素) で監視を始める。コールバックには変化した要素の配列が届き、entry.contentRect.width で幅が取れる。基本はこれだけです。

実装で押さえるべきは3点です。entries は配列なので必ず回すこと、使い終わったら disconnect() で片付けること、そしてコールバックの中でレイアウトを変えないこと。見た目を切り替えるだけなら CSS の @container のほうが適していることも多いので、サイズの数値が必要になったときの選択肢として持っておくとよいでしょう。

参考ページ