MENU

Web API 完全ガイド – IntersectionObserver, MutationObserver など

目次

Web API 完全ガイド – IntersectionObserver, MutationObserver など

Web API がもたらす パフォーマンス向上

React や Vue などのフレームワークを使っていても、ブラウザネイティブの Web API を活用することで、大きなパフォーマンス向上が実現できます。「スクロール検出に scroll イベントを使っている」「DOM 変更を毎秒チェックしている」といった非効率な実装をしていませんか?

今回は、フロントエンド開発でよく使う Web API、特にIntersectionObserver、MutationObserver、ResizeObserver などを詳しく解説し、実装例を通じて効率的なコード構造を見ていきたいと思います。

対象となる方

  • フロントエンド開発でパフォーマンス最適化を考えている方
  • Web API の活用方法を知りたい方
  • scroll、resize イベントの代替案を探している方

 ※ このドキュメントはモダンブラウザ対応を想定しています。

IntersectionObserver – 要素の表示検出

IntersectionObserver の基本

IntersectionObserver は、要素がビューポート(画面に見える領域)に入ったか、出たかを効率的に検出します。scroll イベントよりも圧倒的に高速です。

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      console.log(`${entry.target} は表示されました`);
    } else {
      console.log(`${entry.target} は画面外に出ました`);
    }
  });
});

// 監視対象を指定
const element = document.getElementById('lazy-image');
observer.observe(element);

IntersectionObserver のオプション

root, rootMargin, threshold を調整して、検出タイミングを制御します。

const observer = new IntersectionObserver(
  (entries) => {
    // コールバック
  },
  {
    root: null,           // viewport を使用
    rootMargin: '50px',   // viewport より 50px 手前で検出開始
    threshold: 0.5,       // 要素の 50% が見えたら検出
  }
);

実装例:遅延ロード

画像が画面に入ってから実際に読み込むことで、初期ロード時間を削減します。

import { useEffect, useRef } from 'react';

export function LazyImage({ src, alt }: { src: string; alt: string }) {
  const imgRef = useRef<HTMLImageElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting && imgRef.current) {
          // 画像を読み込む
          imgRef.current.src = src;
          imgRef.current.alt = alt;
          observer.unobserve(imgRef.current);
        }
      });
    });

    if (imgRef.current) {
      observer.observe(imgRef.current);
    }

    return () => observer.disconnect();
  }, [src, alt]);

  return <img ref={imgRef} style={{ height: '400px' }} />;
}

実装例:無限スクロール

export function InfiniteScroll() {
  const [items, setItems] = useState<string[]>([]);
  const [page, setPage] = useState(1);
  const sentinelRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting) {
        // 次ページのデータを取得
        fetch(`/api/items?page=${page}`)
          .then((r) => r.json())
          .then((data) => {
            setItems((prev) => [...prev, ...data]);
            setPage((p) => p + 1);
          });
      }
    });

    if (sentinelRef.current) {
      observer.observe(sentinelRef.current);
    }

    return () => observer.disconnect();
  }, [page]);

  return (
    <>
      {items.map((item) => <div key={item}>{item}</div>)}
      <div ref={sentinelRef} style={{ height: '100px' }} />
    </>
  );
}

MutationObserver – DOM 変更の監視

MutationObserver の基本

MutationObserver は、DOM 要素の追加・削除・属性変更・テキスト変更などを検出します。

const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    if (mutation.type === 'childList') {
      console.log('子要素が追加/削除されました');
    } else if (mutation.type === 'attributes') {
      console.log(`${mutation.attributeName} 属性が変更されました`);
    } else if (mutation.type === 'characterData') {
      console.log('テキストが変更されました');
    }
  });
});

const element = document.getElementById('target');
observer.observe(element, {
  childList: true,        // 子要素の追加・削除を監視
  attributes: true,       // 属性変更を監視
  characterData: true,    // テキスト変更を監視
  subtree: true,          // 子孫要素も監視
});

実装例:リアルタイム通知

export function NotificationCenter() {
  const [notifications, setNotifications] = useState<string[]>([]);
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        if (mutation.type === 'childList') {
          // 新しい通知が追加されたかチェック
          mutation.addedNodes.forEach((node) => {
            if (node.nodeType === 1) {
              // element node
              const text = (node as HTMLElement).textContent;
              if (text) {
                setNotifications((prev) => [...prev, text]);
              }
            }
          });
        }
      });
    });

    if (containerRef.current) {
      observer.observe(containerRef.current, { childList: true });
    }

    return () => observer.disconnect();
  }, []);

  return (
    <>
      <div ref={containerRef} id="notification-container"></div>
      <div>
        {notifications.map((n, i) => (
          <div key={i}>{n}</div>
        ))}
      </div>
    </>
  );
}

ResizeObserver – リサイズ検出

ResizeObserver の基本

ResizeObserver は、要素のサイズ変更を検出します。window の resize イベントよりも効率的で、特定の要素のサイズ変更に対応します。

export function ResponsiveComponent() {
  const [width, setWidth] = useState(0);
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const observer = new ResizeObserver((entries) => {
      entries.forEach((entry) => {
        setWidth(entry.contentRect.width);
      });
    });

    if (containerRef.current) {
      observer.observe(containerRef.current);
    }

    return () => observer.disconnect();
  }, []);

  return (
    <div ref={containerRef}>
      <p>Width: {width}px</p>
      {width > 600 ? (
        <div>Desktop layout</div>
      ) : (
        <div>Mobile layout</div>
      )}
    </div>
  );
}

RequestAnimationFrame – スムーズなアニメーション

RequestAnimationFrame の役割

RequestAnimationFrame(RAF)は、ブラウザの再描画タイミングに合わせてコールバックを実行します。高フレームレートのアニメーションが実現できます。

export function AnimatedCounter() {
  const [value, setValue] = useState(0);

  const animateTo = (target: number) => {
    let current = 0;
    const step = target / 30; // 30フレームで到達

    const animate = () => {
      current += step;
      if (current < target) {
        setValue(Math.floor(current));
        requestAnimationFrame(animate);
      } else {
        setValue(target);
      }
    };

    requestAnimationFrame(animate);
  };

  return (
    <div>
      <p>Value: {value}</p>
      <button onClick={() => animateTo(100)}>Animate</button>
    </div>
  );
}

Fetch API と AbortController

AbortController でリクエストをキャンセル

コンポーネントがアンマウントされるときに、進行中の API リクエストをキャンセルすることが重要です。

export function DataFetcher() {
  const [data, setData] = useState<any>(null);

  useEffect(() => {
    const abortController = new AbortController();

    fetch('/api/data', { signal: abortController.signal })
      .then((r) => r.json())
      .then((data) => {
        setData(data);
      })
      .catch((error) => {
        if (error.name === 'AbortError') {
          console.log('リクエストがキャンセルされました');
        }
      });

    // クリーンアップ:コンポーネント がアンマウントされるときにキャンセル
    return () => abortController.abort();
  }, []);

  return <div>{JSON.stringify(data)}</div>;
}

Web API の比較表

API用途パフォーマンスブラウザ対応
IntersectionObserver要素の表示検出優秀(scroll より高速)モダンブラウザ全対応
MutationObserverDOM 変更検出中(頻繁な変更は注意)IE11+
ResizeObserver要素のリサイズ検出優秀最新ブラウザ
RequestAnimationFrameスムーズなアニメーション優秀全ブラウザ対応
Fetch API非同期通信標準IE 未対応(ポリフィル必要)

実装例:統合的な使用

遅延ロード + パフォーマンス監視

export function OptimizedImageGallery({
  images,
}: {
  images: { id: string; src: string }[];
}) {
  const [loaded, setLoaded] = useState<Set<string>>(new Set());

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            const img = entry.target as HTMLImageElement;
            const id = img.dataset.id!;

            const image = new Image();
            image.src = img.dataset.src!;
            image.onload = () => {
              img.src = image.src;
              setLoaded((prev) => new Set([...prev, id]));
              observer.unobserve(img);
            };
          }
        });
      },
      { rootMargin: '100px' }
    );

    document.querySelectorAll('[data-lazy]').forEach((el) => {
      observer.observe(el);
    });

    return () => observer.disconnect();
  }, []);

  return (
    <div>
      {images.map((img) => (
        <img
          key={img.id}
          data-id={img.id}
          data-src={img.src}
          data-lazy
          alt=""
          style={{
            backgroundColor: loaded.has(img.id) ? 'transparent' : '#e0e0e0',
          }}
        />
      ))}
    </div>
  );
}

まとめ

以上で Web API 完全ガイドを終えたいと思います。

IntersectionObserver、MutationObserver、ResizeObserver などのモダン Web API を活用することで、scroll イベント依存のコードから解放され、大幅なパフォーマンス向上が実現できます。これらの API はブラウザがネイティブに最適化しているため、JavaScript で自分で実装するよりも圧倒的に効率的です。

ぜひこれらの Web API をプロジェクトに組み込んで、ユーザー体験の向上を目指してください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次