👻

JavaScript / IntersectionObserverとResizeObserver

2025/01/27に公開
2

概要

それぞれブラウザが提供しているWeb APIであり、IntersectionObserverは対象要素が描画領域との交差状態を、ResizeObserverは対象要素の幅の大きさをそれぞれ監視(observe)する。

従来の手法の例

要素が描画領域に入った時に処理を発火させる場合(IntersectionObserver以前)

const targetEl = document.getElementById('target');

function handleScroll(el, callback) {

  window.addEventListener('scroll', () => {
    let elPosition = el.getBoundingClientRect().top;
    let windowHeight = window.innerHeight;

    if (elPosition <= windowHeight) {
      callback();
    }
  });
}

handleScroll(targetEl, () => console.log("Hello")); 

ブラウザ幅の判定結果によって処理を発火させる場合(ResizeObserver以前)

function handleResize(callback) {

  window.addEventListener('resize', () => {
    const isPc = window.innerWidth > 750;
    if (isPc) {
      callback();
    }
  });
}

handleResize(() => console.log('Hello'));

従来の手法のデメリット

1. ブラウザに負担がかかる

スクロールイベントやリサイズイベントは高頻度で発火する。各イベントごとに処理を実行するとメインスレッドを占有してパフォーマンスが著しく落ちる。(それこそレンダリングが遅延するなど眼に見える形で悪影響がでる。)

2. ロジックが人によって異なる

結構これも大きいと思う。自由にロジックが組めることによってコード品質差が生まれてしまう。
余分なロジックが記述されることでメンテナンス性を損なったり、デバウンスがカットされることで上記1のデメリットが引き起こされる事態を引き起こすなど、様々な弊害がでるだろう。

IntersectionObserverとResizeObserverを利用するメリット

1. DOMやイベントを非同期で監視する

メインスレッドを占有することなく対象を監視する。

2. 処理発火頻度の最適化

ブラウザが必要だと判断した時だけコールバックを実行するため、無駄な処理が抑えられる。

3. デバウンスなど複雑な処理を加える必要がない

コードが簡略化され可読性が高まるり、保守性がアップする。

IntersectionObserverの使い方


// 監視対象の要素を取得
const targetEl = document.getElementById('target');

// オブザーバーのインスタンスを作成。
// 引数に監視状態が変化した際に実行される関数を渡す。
const observer = new IntersectionObserver(callback);
// observeメソッドで監視を開始
observer.observe(targetEl);

// 監視状態が変化した際に実行される関数。引数のentriesは監視対象を表す
function callback(entries) {
  // 状態を表すプロパティへアクセスできる
  if (entries[0].intersectionRatio !== 0) {
    console.log("hello")
  }
};

RisezeObserverの使い方

// 監視対象の要素を取得
const bodyEl = document.body;

// オブザーバーのインスタンスを作成。
// 引数に監視状態が変化した際に実行される関数を渡す。
const observer = new ResizeObserver(callback);
// observeメソッドで監視を開始
observer.observe(bodyEl);

// 監視状態が変化した際に実行される関数。引数のentriesは監視対象を表す
function callback(entries) {
  entries.forEach(entry => {
    // 状態を表すプロパティへアクセスできる
    const { width } = entry.contentRect;

    if (width > 750) {
      console.log("Hello");
    }
  });
}

注意点

いくらメインスレッドへの負担が減るといっても、一度に大量のイベントが発火するor重い処理を頻繁に発火させるとメインスレッドへの負担が増すことに変わりはないので、使い方・使い所には注意が必要。

Discussion

junerjuner

むしろ resize イベントの監視を 要素単位に変更したのが RisezeObserver で、
別のアプローチとして matchMedia がある感じでしょうか……?

https://developer.mozilla.org/ja/docs/Web/API/Window/matchMedia

ちなみに matchMedia は css の @media (ウィンドウレベル) なので、各要素版の @container に対応したのは 現在策定中です。

https://github.com/w3c/csswg-drafts/issues/6205

つむぐつむぐ

むしろ resize イベントの監視を 要素単位に変更したのが RisezeObserver で、別のアプローチとして matchMedia がある感じでしょうか……?

おっしゃる通りで、ResizeObserverは<div>や<p>など要素単位でイベントの監視を行います。
matchMediaは昔一回ぐらいしか使ったことがないので恐縮なのですが、「CSSのメディアクエリを利用してブラウザの状態を監視するもの」と理解しています。同期or非同期の違いはありますが。
(今回RisezeObserverの監視対象をブラウザ幅にしていたので、例としてよろしくなかったかもしれませんね!単純にブラウザ幅を監視するだけならmatchMediaの方がいいなぁ、、、勉強になります笑)

ちなみに matchMedia は css の @media (ウィンドウレベル) なので、各要素版の @container に対応したのは 現在策定中です。

そうなのですね!教えていただきありがとうございます!