Intersection Observer

要素がビューポートに入ったことを非同期で検知する Web API

APIパフォーマンス

Intersection Observer とは

Intersection Observer は、要素がビューポート (または指定したコンテナ) に入った/出たことを非同期で検知する Web API である。scroll イベントで自前に判定する方式と違い、交差の計算はブラウザ側でまとめて行われるため、スクロールのたびに getBoundingClientRect() を呼んでレイアウトを再計算させる必要がない。

ただし軽くなるのは判定部分だけで、コールバック自体はメインスレッドで実行される。ここに重い DOM 操作を書けば描画は普通に詰まるので、時間のかかる処理は requestIdleCallback() へ逃がす。

scroll イベントとの比較

scroll イベントとの主な違いを以下に比較する。

観点scroll イベントIntersection Observer
交差の判定スクロールごとに自前で計算 (レイアウト再計算を招きやすい)ブラウザ側でまとめて計算
コールバックの実行メインスレッドメインスレッド (軽くなるのは判定側だけ)
発火の頻度スクロール量に比例 (自前でスロットリングが必要)交差率が threshold を跨いだときだけ
判定の指定方法ピクセル単位で自前計算交差率 (threshold) と余白 (rootMargin) で宣言

基本的な使い方

基本的な使い方のコード例を示す。

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add('visible');
      observer.unobserve(entry.target); // 一度だけ検知
    }
  });
}, { threshold: 0.1 }); // 交差率が 10% を跨いだ時点で発火 (画面外へ出るときも発火する)

document.querySelectorAll('.lazy').forEach(el => observer.observe(el));

observe() を呼んだ直後、対象が画面外にあってもコールバックは一度実行される (初期状態の通知)。「呼ばれた = 表示された」と解釈して entry.isIntersecting の判定を省くと、ページ読み込み直後に全要素が表示済みとして処理されてしまう。遅延読み込みの意味が消えるので、分岐は必ず入れる。

画像の遅延読み込み

画像の遅延読み込みのコード例を示す。

const imgObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target as HTMLImageElement;
      img.src = img.dataset.src!;
      imgObserver.unobserve(img);
    }
  });
}, { rootMargin: '200px' }); // 200px 手前で読み込み開始

document.querySelectorAll('img[data-src]').forEach(img => imgObserver.observe(img));

rootMargin: '200px' で、ビューポートの 200px 手前から読み込みを開始し、スクロール時に画像が表示される前にロードを完了する。

無限スクロール

無限スクロールのコード例を示す。

const sentinel = document.getElementById('sentinel')!;

const observer = new IntersectionObserver(async ([entry]) => {
  if (entry.isIntersecting) {
    const items = await fetchNextPage();
    renderItems(items);
  }
});

observer.observe(sentinel); // リストの末尾に配置した要素を監視

スクロール連動アニメーション

スクロール連動アニメーションのコード例を示す。

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add('fade-in');
    }
  });
}, { threshold: 0.2 });

document.querySelectorAll('.section').forEach(el => observer.observe(el));
.section { opacity: 0; transform: translateY(30px); transition: all 0.5s ease; }
.section.fade-in { opacity: 1; transform: translateY(0); }

React での使い方

React での使い方のコード例を示す。

function useInView(ref: RefObject<HTMLElement>) {
  const [inView, setInView] = useState(false);
  useEffect(() => {
    if (!ref.current) return;
    const observer = new IntersectionObserver(([entry]) => setInView(entry.isIntersecting));
    observer.observe(ref.current);
    return () => observer.disconnect();
  }, [ref]);
  return inView;
}

オプション

主なオプションを以下にまとめる。

オプション説明
root交差を判定するコンテナ (デフォルト: ビューポート)
rootMarginroot の余白 ('200px' で手前から検知)。単位は px と % のみで、em / rem は指定できない
threshold発火させる交差率。境界を上下どちらの方向に跨いでも発火する。配列で複数指定できる ([0, 0.5, 1])

Intersection Observer の理解を深めるには関連書籍が参考になる。

この記事は役に立ちましたか?

関連用語

関連する記事