MutationObserverで動的DOMを監視する基本的な使い方と注意点

javascript
記事内に広告が含まれています。

「DOM の変化をどう検知していますか?」——setInterval でポーリングしていると、パフォーマンスもコードの見通しも悪化しがちです。MutationObserver は、DOM の追加・削除・属性変更などを効率的に検知できるブラウザ標準 API。この記事では、基本構文から実践パターンまでを網羅します。

動的な Web サイトでは、Ajax/Fetch による読み込みや SPA での画面遷移、広告・タグ管理ツールによる DOM 操作など、ページ表示後に要素が追加・変更されるケースが日常的です。こうした変化に反応して「イベントを付与する」「解析タグを発火する」「遅延読み込みを適用する」といった処理を、従来のポーリングやイベント任せで行うと、無駄な実行や競合、メモリリークのリスクが高まります。

この記事を読んでわかること

  • MutationObserver の概要と、DOM の追加・削除・属性変更を検知する仕組み
  • 基本的な使い方:new MutationObserver() の構文、監視オプション(childList/attributes/characterData/subtree 等)、MutationRecord の活用方法
  • 実践的なユースケース:Ajax/Fetch 後の要素検知、動的ボタンのイベント設定、モーダルの class 変更監視、無限スクロール、Lazy Load、WordPress/GTM/拡張機能の動的コンテンツ監視
  • 他手法との比較:setInterval ポーリングとの違い、通常イベントで十分なケース、最適な DOM 監視手法の選び方
  • 安全に使うための注意点:監視範囲の最小化、適切なオプション設定、disconnect() によるクリーンアップ
  • 実装時の落とし穴回避のポイント

「効率的なDOM監視の実装パターンをマスターしたい」「無駄のないクリーンなコードを書けるようになりたい」という方は、ぜひ参考にしてみてください!

格安ドメイン取得サービス─ムームードメイン─

MutationObserverとは?

mutation observer

MutationObserverの概要

MutationObserverは、HTML要素(DOM)の追加・削除・属性変更を監視し、変更が起きたときに自動的に指定した関数を実行するJavaScriptの標準APIです。

DOMとは、HTMLをブラウザが扱えるツリー構造として表現したものです。<div><p>といったタグ一つ一つがノードとして扱われ、これらが階層関係を持つ構造になっています。ページを開いた後で、このDOM構造が変わることを「DOM変更」と呼びます。

MutationObserverは、このDOM変更をリアルタイムに検知して対応できるツールです。例えば、「新しく追加されたボタンにクリックイベントを設定したい」「動的に変わる要素のクラス名を監視したい」といった用途で活躍します。

MutationObserver - Web API | MDN
MutationObserver インターフェイスは、 DOM ツリーへ変更が加えられたことを監視することができる機能を提供します。これは DOM3 Events の仕様で定義されていた Mutation Events 機能の置き換えとして設計されたものです。

DOMの追加・削除・属性変更を検知する仕組み

MutationObserverがDOM変更を検知する仕組みを理解するために、具体的な例で見てみましょう。

<div id="container">
  <p>既存のテキスト</p>
</div>

<button id="add-button">新規追加</button>
// 監視対象を指定
const container = document.getElementById('container');

// 監視処理を作成
const observer = new MutationObserver((mutations) => {
  console.log('DOM変更が検知されました');
  console.log(mutations); // 変更情報が配列で渡される
});

// 監視を開始
observer.observe(container, {
  childList: true  // 子要素の追加・削除を監視
});

// ボタンをクリックして新しい要素を追加
document.getElementById('add-button').addEventListener('click', () => {
  const newParagraph = document.createElement('p');
  newParagraph.textContent = '新しく追加されたテキスト';
  container.appendChild(newParagraph);
  // この時点でMutationObserverのコールバック関数が自動的に実行される
});

このコードでは、ボタンをクリックしてcontainerに新しい<p>要素が追加されると、MutationObserverのコールバック関数が自動的に実行されます。ブラウザが変更を検知して、登録された関数を呼んでくれるということです。

MutationObserverが検知できるDOM変更には複数の種類があります。

  • childList: 子要素の追加・削除
  • attributes: 属性値の変更(例:classdata-*styleなど)
  • characterData: テキスト内容の変更
  • subtree: 監視対象要素の配下全体の変更

後ほど詳しく説明しますが、監視する内容に応じてオプションを組み合わせることで、必要な変更だけを監視できます。

MutationObserverを使うべきケース・使わないケース

MutationObserverは便利なツールですが、すべてのDOM監視に使うべきではありません。適切な場面で活用することが重要です。

MutationObserverを使うべきケース

  • Ajaxなどによって動的に追加されたHTML要素を検知したい
  • 外部スクリプト(Google Tag Manager、ブラウザ拡張機能など)による予測不能なDOM変更に対応する必要がある
  • 多くの要素に動的にイベントを設定する必要があり、その追加タイミングが不確定
  • モーダルやメニューなど、他のスクリプトが操作する可能性のある要素の変更を監視したい
  • WordPressプラグインなど、制御できないスクリプトが追加する要素に対応する

これらの場合、MutationObserverを使うことで対応コストを下げられます。

MutationObserverを使わないべきケース

  • ユーザーがボタンをクリックした、テキストボックスに入力した、といった明確なユーザー操作がある場合は、通常のclickinputイベントリスナーを使う
  • 新しく追加される要素が確定している場合は、イベントデリゲーションを活用する方が効率的
  • 単に定期的に状態をチェックしたいだけなら、setIntervalの方が単純かもしれない(ただしパフォーマンスは劣る)
  • 要素が表示領域に入ったかを検知したいなら、Intersection Observerを使う

特に重要な点は、動的な要素にイベントを設定する場合、必ずしもMutationObserverが最適ではないということです。イベントデリゲーションが使える場面では、そちらの方が実装がシンプルで、パフォーマンスも良いです。後の章で詳しく比較します。

MutationObserverの基本的な使い方

mutation observer

new MutationObserver()で監視処理を作る基本構文

MutationObserverを使う流れは単純です。まず監視処理を作成し、次に監視対象を指定して実行を開始するという2つのステップです。

// ステップ1: 監視処理を作成
const observer = new MutationObserver((mutations) => {
  // mutations は配列。その中身はMutationRecord オブジェクト
  console.log('DOM変更が発生しました');
  console.log(mutations);
});

// ステップ2: 監視対象を指定して監視を開始
const targetElement = document.getElementById('target');
observer.observe(targetElement, {
  childList: true  // 子要素の追加・削除を監視
});

// ステップ3: 監視を終了する(必要なときに)
observer.disconnect();

このコードの流れを説明します。

new MutationObserver()は、DOM変更が検知されたときに実行される関数(コールバック)を受け取ります。この関数にはmutationsという配列が渡され、その配列の中に変更情報が入っています。

observer.observe()で監視対象と監視内容を指定します。第一引数は監視する要素のDOM参照、第二引数は監視オプションです。

observer.disconnect()は監視を終了するメソッドです。ページから離脱するときやコンポーネントが破棄されるときは、必ず呼び出してメモリリークを防ぎます。

実際に動作するコード例を見てみましょう。

<!DOCTYPE html>
<html>
<head>
  <title>MutationObserverの基本例</title>
</head>
<body>
  <div id="container">
    <p>初期テキスト</p>
  </div>

  <button id="add-btn">要素追加</button>
  <button id="remove-btn">要素削除</button>

  <script>
    // 監視処理を作成
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        if (mutation.type === 'childList') {
          console.log('子要素が変更されました');
          if (mutation.addedNodes.length > 0) {
            console.log('追加されたノード:', mutation.addedNodes);
          }
          if (mutation.removedNodes.length > 0) {
            console.log('削除されたノード:', mutation.removedNodes);
          }
        }
      });
    });

    // 監視対象を指定
    const container = document.getElementById('container');
    observer.observe(container, {
      childList: true  // 子要素の追加・削除のみを監視
    });

    // 要素追加ボタンのクリック処理
    document.getElementById('add-btn').addEventListener('click', () => {
      const newElement = document.createElement('p');
      newElement.textContent = '新規追加: ' + new Date().toLocaleTimeString();
      container.appendChild(newElement);
    });

    // 要素削除ボタンのクリック処理
    document.getElementById('remove-btn').addEventListener('click', () => {
      if (container.children.length > 1) {
        container.removeChild(container.lastChild);
      }
    });

    // ページ離脱時に監視を終了
    window.addEventListener('beforeunload', () => {
      observer.disconnect();
    });
  </script>
</body>
</html>

このコードでは、「要素追加」ボタンをクリックするたびにMutationObserverが変更を検知し、コンソールにメッセージを出力します。コールバック内でmutation.addedNodesmutation.removedNodesを確認することで、追加・削除されたノードを特定できます。

監視オプション(childList・attributes・characterData・subtree等)の設定例

MutationObserverの監視オプションは、何をどのように監視するかを細かく制御するものです。各オプションを見ていきましょう。

childList

子要素の追加・削除を監視します。最も基本的なオプションです。

observer.observe(element, {
  childList: true
});

attributes

属性値の変更を監視します。例えばclass属性やdata-*属性の変更を検知します。

observer.observe(element, {
  attributes: true  // すべての属性の変更を監視
});

属性変更を監視する際、attributeFilterを組み合わせることで、特定の属性のみに限定できます。これにより、無駄な監視を減らしてパフォーマンスが向上します。

observer.observe(element, {
  attributes: true,
  attributeFilter: ['class', 'data-status']  // この2つの属性のみ監視
});

属性の変更前の値を取得したい場合は、attributeOldValueを追加します。

observer.observe(element, {
  attributes: true,
  attributeOldValue: true  // 変更前の属性値を記録
});

characterData

テキスト内容(TextNodeの内容)の変更を監視します。<p>テキスト</p>の「テキスト」の部分が変わることを検知します。

observer.observe(textNode, {
  characterData: true
});

テキスト変更前の値を取得するには、characterDataOldValueを指定します。

observer.observe(textNode, {
  characterData: true,
  characterDataOldValue: true
});

subtree

監視対象要素の配下全体を監視します。これまでは直下の子要素のみ監視していましたが、subtree: trueにすると、階層の深さに関わらず、すべての子孫要素の変更を監視します。

observer.observe(element, {
  childList: true,
  subtree: true  // 配下全体の子要素追加・削除を監視
});

実践的な組み合わせ例

実務では、複数のオプションを組み合わせて使います。

<div id="card" class="card">
  <h2>カード</h2>
  <button class="btn">ボタン</button>
</div>

<script>
  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      console.log(`変更タイプ: ${mutation.type}`);
    });
  });

  // カード要素とその配下の変更を監視
  // 子要素の追加・削除、class属性の変更を検知
  document.getElementById('card').observe(
    {
      childList: true,      // 子要素の追加・削除を監視
      attributes: true,     // 属性変更を監視
      attributeFilter: ['class'],  // ただしclass属性のみ
      subtree: true         // 配下全体を監視
    }
  );
</script>

注意点として、subtree: trueは監視範囲を大きくするため、変更が多い場面では監視対象の要素が自身も変更を検知する可能性があります。この問題は後の章で詳しく説明します。

MutationRecordを使って追加・削除ノードや変更前の属性値を取得する方法

MutationRecordは、DOM変更の詳細情報を持つオブジェクトです。MutationObserverのコールバック関数に渡されるmutations配列の各要素がMutationRecordオブジェクトです。

MutationRecordには、以下のプロパティがあります。

プロパティ説明
type変更の種類(’childList’、’attributes’、’characterData’)
target変更された要素へのDOM参照
addedNodes追加されたノードのリスト(NodeList)
removedNodes削除されたノードのリスト(NodeList)
attributeName変更された属性名(attributes型の場合)
oldValue変更前の値(attributeOldValue or characterDataOldValue指定時)
nextSibling追加・削除されたノードの次の兄弟要素
previousSibling追加・削除されたノードの前の兄弟要素

具体的なコード例を見てみましょう。

<div id="container">
  <p>段落1</p>
  <p>段落2</p>
</div>

<button id="add-btn">追加</button>
<button id="change-btn">class変更</button>

<script>
  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      console.log('--- MutationRecord情報 ---');
      console.log('変更タイプ:', mutation.type);
      console.log('変更対象:', mutation.target);

      if (mutation.type === 'childList') {
        // 子要素の追加・削除の場合
        if (mutation.addedNodes.length > 0) {
          console.log('追加されたノード数:', mutation.addedNodes.length);
          mutation.addedNodes.forEach((node) => {
            if (node.nodeType === Node.ELEMENT_NODE) {
              console.log('追加要素:', node.tagName, node.textContent);
            }
          });
        }

        if (mutation.removedNodes.length > 0) {
          console.log('削除されたノード数:', mutation.removedNodes.length);
          mutation.removedNodes.forEach((node) => {
            if (node.nodeType === Node.ELEMENT_NODE) {
              console.log('削除要素:', node.tagName, node.textContent);
            }
          });
        }
      }

      if (mutation.type === 'attributes') {
        // 属性変更の場合
        console.log('変更された属性:', mutation.attributeName);
        console.log('新しい属性値:', mutation.target.getAttribute(mutation.attributeName));
        if (mutation.oldValue !== null) {
          console.log('変更前の属性値:', mutation.oldValue);
        }
      }
    });
  });

  const container = document.getElementById('container');
  observer.observe(container, {
    childList: true,
    attributes: true,
    attributeFilter: ['class'],
    attributeOldValue: true,
    subtree: true
  });

  // 要素追加
  document.getElementById('add-btn').addEventListener('click', () => {
    const newPara = document.createElement('p');
    newPara.textContent = '新規段落';
    container.appendChild(newPara);
  });

  // class属性変更
  document.getElementById('change-btn').addEventListener('click', () => {
    container.className = container.className ? '' : 'active';
  });
</script>

このコードを実行すると、以下のような動作が確認できます。

  • 要素追加ボタンをクリック:type が 'childList' で、addedNodes に新しい<p>要素が含まれます
  • class変更ボタンをクリック:type が 'attributes' で、attributeName'class' になり、oldValue には変更前のクラス名が保存されます

重要な点として、addedNodesremovedNodesNodeListという配列ライクなオブジェクトです。テキストノードも含まれるため、要素かテキストかを判定するにはnode.nodeType === Node.ELEMENT_NODEでチェックする必要があります。

次の例では、変更前後の属性値を利用して、実務に近い処理を示します。

const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    if (mutation.type === 'attributes' && mutation.attributeName === 'data-status') {
      const newStatus = mutation.target.getAttribute('data-status');
      const oldStatus = mutation.oldValue;

      console.log(`ステータスが「${oldStatus}」から「${newStatus}」に変わりました`);

      // ステータス変更に応じた処理
      if (newStatus === 'active') {
        mutation.target.style.backgroundColor = 'lightgreen';
      } else if (newStatus === 'error') {
        mutation.target.style.backgroundColor = 'lightcoral';
      }
    }
  });
});

const element = document.getElementById('status-element');
observer.observe(element, {
  attributes: true,
  attributeFilter: ['data-status'],
  attributeOldValue: true
});

このようにMutationRecordの情報を活用することで、DOM変更に応じた細かい制御が可能になります。

MutationObserverの実践的な使い方

Ajax・Fetchで追加されたHTML要素を検知する

AjaxやFetch APIでサーバーから新しいHTMLを取得してDOM に挿入する場面は、実務では頻繁にあります。こうした場合、追加されたHTML要素に対して何らかの処理を行う必要があることが多いです。MutationObserverはこのユースケースに最適です。

例えば、ボタンをクリックするとFetchで記事一覧を読み込んで、#article-listに追加する場合を考えます。

<div id="article-list">
  <!-- ここに記事がFetchで追加される -->
</div>

<button id="load-more">もっと読み込む</button>

<script>
  // Fetchで追加された要素に対応する監視処理
  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      if (mutation.type === 'childList') {
        mutation.addedNodes.forEach((node) => {
          // 追加されたのが要素ノードか確認
          if (node.nodeType === Node.ELEMENT_NODE) {
            // 追加された要素に対して処理を実行
            console.log('新しい記事要素が追加されました:', node);

            // 例えば、タイトルの最初の文字を大文字にする処理
            const titleElement = node.querySelector('h3');
            if (titleElement) {
              titleElement.style.fontWeight = 'bold';
            }
          }
        });
      }
    });
  });

  // 記事リストの監視を開始
  const articleList = document.getElementById('article-list');
  observer.observe(articleList, {
    childList: true,
    subtree: true  // 記事内部の構造も変わる可能性があるため
  });

  // もっと読み込むボタンのクリック処理
  document.getElementById('load-more').addEventListener('click', async () => {
    try {
      const response = await fetch('/api/articles');
      const html = await response.text();

      // サーバーから取得したHTMLをDOMに追加
      // この時点でMutationObserverが自動的に反応
      articleList.insertAdjacentHTML('beforeend', html);
    } catch (error) {
      console.error('読み込み失敗:', error);
    }
  });
</script>

このコードの流れを説明します。

  1. MutationObserverで#article-listの子要素の追加を監視
  2. ボタンをクリックするとFetch APIでHTMLを取得
  3. insertAdjacentHTML()でDOM に追加
  4. MutationObserverが自動的に新しい要素を検知し、コールバック関数を実行
  5. 追加された要素に対して必要な処理を行う

このアプローチの利点は、Fetchがいつ完了するか、どんな構造のHTMLが返ってくるかをあらかじめ完全には予測できない場合でも対応できるという点です。

動的に生成されたボタンへイベントを設定する

動的に追加されたボタンにクリックイベントを設定したい場合、MutationObserverとイベントデリゲーションの2つのアプローチがあります。ここでは両方を説明し、どう使い分けるかを示します。

アプローチ1:MutationObserverで新しい要素を検知してイベントを設定

<div id="button-container">
  <button class="action-btn">既存のボタン</button>
</div>

<button id="add-new-btn">新しいボタンを追加</button>

<script>
  // ボタンにクリックイベントを設定する関数
  function attachButtonEvent(button) {
    button.addEventListener('click', function() {
      console.log('ボタンがクリックされました:', this.textContent);
      this.style.backgroundColor = 'lightblue';
    });
  }

  // 既存のボタンにイベントを設定
  document.querySelectorAll('.action-btn').forEach(attachButtonEvent);

  // 新しく追加されたボタンにイベントを設定する監視処理
  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      if (mutation.type === 'childList') {
        mutation.addedNodes.forEach((node) => {
          if (node.nodeType === Node.ELEMENT_NODE) {
            // 追加された要素が .action-btn クラスを持つ場合
            if (node.classList && node.classList.contains('action-btn')) {
              attachButtonEvent(node);
            }
            // または、追加された要素の中から .action-btn を探す
            const buttons = node.querySelectorAll?.('.action-btn') || [];
            buttons.forEach(attachButtonEvent);
          }
        });
      }
    });
  });

  const container = document.getElementById('button-container');
  observer.observe(container, {
    childList: true,
    subtree: true
  });

  // 新しいボタンを追加(この時点でMutationObserverが反応)
  document.getElementById('add-new-btn').addEventListener('click', () => {
    const newButton = document.createElement('button');
    newButton.className = 'action-btn';
    newButton.textContent = '新しいボタン(' + new Date().toLocaleTimeString() + ')';
    container.appendChild(newButton);
  });
</script>

アプローチ2:イベントデリゲーションを使う(推奨)

<div id="button-container">
  <button class="action-btn">既存のボタン</button>
</div>

<button id="add-new-btn">新しいボタンを追加</button>

<script>
  // 親要素にイベントリスナーを1つ設定
  // クリックが親に伝わってきたときに、対象が .action-btn かを確認
  const container = document.getElementById('button-container');

  container.addEventListener('click', (event) => {
    if (event.target.classList.contains('action-btn')) {
      console.log('ボタンがクリックされました:', event.target.textContent);
      event.target.style.backgroundColor = 'lightblue';
    }
  });

  // 新しいボタンを追加(MutationObserverは不要)
  document.getElementById('add-new-btn').addEventListener('click', () => {
    const newButton = document.createElement('button');
    newButton.className = 'action-btn';
    newButton.textContent = '新しいボタン(' + new Date().toLocaleTimeString() + ')';
    container.appendChild(newButton);
  });
</script>

どちらを使うべきか

イベントデリゲーションが使える場合は、イベントデリゲーションを優先してください。理由は以下の通りです。

  • 実装がシンプル:MutationObserverのコールバック内でイベント設定処理を書く必要がない
  • パフォーマンスが良い:リスナー数が少なくて済む
  • コードが読みやすい:監視と処理が分離されていないため、意図が明確

ただし、イベントデリゲーションが使えないケースがあります。

  • 動的に追加される要素に固有のイベントリスナーが必要な場合(例:要素ごとに異なるIDやデータを保持する必要がある)
  • イベントの伝播を阻止(stopPropagation())したい場合
  • addEventListenerの第三引数(オプション)で特殊な設定が必要な場合

こうした場合のみMutationObserverを使ってください。

モーダル・ポップアップのclass変更を監視する

モーダルやポップアップのような要素は、外部スクリプト(プラグイン、ライブラリなど)によってクラス属性が動的に変更されることがあります。例えば、「class が modal-open に変わったときに背景を暗くする」といった処理が必要な場合、MutationObserverでclass変更を監視できます。

<style>
  .modal {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: white;
    z-index: 1000;
  }

  .modal.open {
    display: block;
  }

  body.modal-open {
    overflow: hidden;
  }
</style>

<div id="modal" class="modal">
  <div class="modal-content">
    <h2>モーダルウィンドウ</h2>
    <button class="close-btn">閉じる</button>
  </div>
</div>

<button id="open-modal-btn">モーダルを開く</button>

<script>
  const modal = document.getElementById('modal');
  const body = document.body;

  // class属性の変更を監視
  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
        const hasOpenClass = modal.classList.contains('open');

        console.log('モーダルのclass属性が変更されました');
        console.log('openクラスの有無:', hasOpenClass);

        // モーダル開閉時に背景のスクロール禁止を切り替え
        if (hasOpenClass) {
          body.classList.add('modal-open');
          console.log('モーダルが開きました');
        } else {
          body.classList.remove('modal-open');
          console.log('モーダルが閉じました');
        }

        // その他、開閉に応じた処理を実行
        if (hasOpenClass) {
          // フォーカス管理など
          modal.querySelector('.close-btn').focus();
        }
      }
    });
  });

  // モーダルのclass変更を監視
  observer.observe(modal, {
    attributes: true,
    attributeFilter: ['class']  // class属性のみ監視してパフォーマンスを最適化
  });

  // モーダルを開く処理
  // (外部スクリプトが同じ処理をする可能性もある)
  document.getElementById('open-modal-btn').addEventListener('click', () => {
    modal.classList.add('open');
  });

  // モーダルを閉じる処理
  modal.querySelector('.close-btn').addEventListener('click', () => {
    modal.classList.remove('open');
  });

  // ページ離脱時にクリーンアップ
  window.addEventListener('beforeunload', () => {
    observer.disconnect();
  });
</script>

このコードでは、attributeFilter: ['class']を指定することで、class属性の変更のみを監視し、不要な監視を避けています。これにより、パフォーマンスを損なわずに実装できます。

無限スクロールで追加されたコンテンツを検知する

無限スクロール機能では、ユーザーがページの下部に近づくと自動的に次の内容が読み込まれます。Intersection Observer を使ってトリガー要素が表示領域に入ったことを検知し、Fetch で新しいコンテンツを読み込み、MutationObserverで追加されたコンテンツを検知するという組み合わせが典型的です。

<style>
  #content {
    margin: 20px;
  }

  .item {
    border: 1px solid #ccc;
    padding: 10px;
    margin-bottom: 10px;
    background: #f9f9f9;
  }

  #loading {
    text-align: center;
    padding: 20px;
    color: #999;
    display: none;
  }

  #loading.active {
    display: block;
  }
</style>

<div id="content">
  <div class="item">アイテム 1</div>
  <div class="item">アイテム 2</div>
  <div class="item">アイテム 3</div>
</div>

<div id="loading">読み込み中...</div>
<div id="trigger"></div>

<script>
  const content = document.getElementById('content');
  const loading = document.getElementById('loading');
  const trigger = document.getElementById('trigger');

  let isLoading = false;
  let pageNumber = 1;

  // MutationObserver: 新しいコンテンツが追加されたことを検知
  const mutationObserver = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
        console.log('新しいアイテムが追加されました');

        // 追加されたアイテムに対して処理を実行
        mutation.addedNodes.forEach((node) => {
          if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('item')) {
            // 例:画像の遅延ロード処理など
            const images = node.querySelectorAll('img');
            images.forEach((img) => {
              img.loading = 'lazy';
            });
          }
        });

        isLoading = false;
        loading.classList.remove('active');
      }
    });
  });

  mutationObserver.observe(content, {
    childList: true
  });

  // Intersection Observer: トリガー要素が表示領域に入ったことを検知
  const intersectionObserver = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting && !isLoading) {
        console.log('ページ下部に近づきました。次のページを読み込みます');
        loadMoreContent();
      }
    });
  }, {
    rootMargin: '100px'  // 要素がビューポートに入る100px前から検知
  });

  intersectionObserver.observe(trigger);

  // 次のコンテンツを読み込む関数
  async function loadMoreContent() {
    if (isLoading) return;

    isLoading = true;
    loading.classList.add('active');

    try {
      pageNumber++;
      const response = await fetch(`/api/items?page=${pageNumber}`);
      const html = await response.text();

      // DOMに追加(MutationObserverが自動的に反応)
      content.insertAdjacentHTML('beforeend', html);
    } catch (error) {
      console.error('読み込み失敗:', error);
      isLoading = false;
      loading.classList.remove('active');
    }
  }

  // クリーンアップ
  window.addEventListener('beforeunload', () => {
    mutationObserver.disconnect();
    intersectionObserver.disconnect();
  });
</script>

このコードのポイントは、Intersection Observer と MutationObserver を組み合わせているという点です。

  • Intersection Observer:ページ下部への到達を検知 → Fetchで次のコンテンツを読み込み
  • MutationObserver:追加されたコンテンツを検知 → 遅延ロード設定など、追加要素への処理を実行

この組み合わせにより、無限スクロール機能を安全に実装できます。

動的に追加された画像へLazy Loadを適用する

サーバーからFetchで取得したHTMLに含まれる画像に対して、遅延ロード(Lazy Load)を適用したい場合があります。MutationObserverで新しい画像要素を検知し、遅延ロード設定を行います。

<style>
  img {
    max-width: 100%;
    height: auto;
  }

  img.loading {
    background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
    background-size: 200% 100%;
    animation: loading 1.5s infinite;
  }

  @keyframes loading {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
  }
</style>

<div id="gallery">
  <img src="image1.jpg" alt="画像1">
  <img src="image2.jpg" alt="画像2">
</div>

<button id="load-more-images">さらに読み込む</button>

<script>
  const gallery = document.getElementById('gallery');

  // MutationObserver: 新しい画像要素を検知
  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      if (mutation.type === 'childList') {
        mutation.addedNodes.forEach((node) => {
          if (node.nodeType === Node.ELEMENT_NODE) {
            // 追加された画像要素を処理
            const images = node.tagName === 'IMG'
              ? [node]
              : node.querySelectorAll('img');

            images.forEach((img) => {
              applyLazyLoad(img);
            });
          }
        });
      }
    });
  });

  observer.observe(gallery, {
    childList: true,
    subtree: true
  });

  // 遅延ロードを適用する関数
  function applyLazyLoad(img) {
    // Intersection Observerで画像がビューポートに入ったかを検知
    if ('loading' in img) {
      // ネイティブLazy Load対応ブラウザ
      img.loading = 'lazy';
    } else {
      // フォールバック処理
      const imageObserver = new IntersectionObserver((entries, observer) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            const img = entry.target;
            img.src = img.dataset.src || img.src;
            img.classList.remove('loading');
            observer.unobserve(img);
          }
        });
      });

      img.classList.add('loading');
      imageObserver.observe(img);
    }
  }

  // 新しい画像をFetchで読み込んで追加
  document.getElementById('load-more-images').addEventListener('click', async () => {
    try {
      const response = await fetch('/api/images');
      const html = await response.text();
      gallery.insertAdjacentHTML('beforeend', html);
      // MutationObserverが新しい画像を自動的に検知して処理
    } catch (error) {
      console.error('読み込み失敗:', error);
    }
  });

  // クリーンアップ
  window.addEventListener('beforeunload', () => {
    observer.disconnect();
  });
</script>

このコードでは、MutationObserverが追加された画像要素を検知し、ネイティブのloading="lazy"か、Intersection Observerによる遅延ロードを適用しています。

WordPress・GTM・ブラウザ拡張機能の動的コンテンツを監視する

WordPressのプラグインやGoogle Tag Manager(GTM)、ブラウザ拡張機能などは、制御不可能な外部スクリプトがDOMを変更します。こうした場合、MutationObserverは不可欠なツールです。

<div id="page-content">
  <h1>ページタイトル</h1>
  <p>ページコンテンツ</p>
  <!-- WordPressプラグインが広告やウィジェットを追加する可能性がある -->
</div>

<script>
  const pageContent = document.getElementById('page-content');

  // 予期しないDOM変更を監視して記録する
  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      if (mutation.type === 'childList') {
        mutation.addedNodes.forEach((node) => {
          if (node.nodeType === Node.ELEMENT_NODE) {
            // 追加された要素をログに記録
            console.log('外部スクリプトが要素を追加しました:', node);

            // 例:プラグインが追加した広告を検知して処理
            if (node.className && node.className.includes('advertisement')) {
              console.log('広告が追加されました');
              // 広告に対して独自のスタイルを適用するなど
              node.style.margin = '20px 0';
            }

            // 例:特定のクラスが付与された要素を処理
            if (node.classList.contains('external-widget')) {
              // ウィジェットの初期化処理
              initializeExternalWidget(node);
            }
          }
        });
      }

      if (mutation.type === 'attributes') {
        // GTMなどが属性を変更した場合を検知
        console.log('属性が変更されました:', mutation.attributeName);
        console.log('対象要素:', mutation.target);
      }
    });
  });

  observer.observe(pageContent, {
    childList: true,
    attributes: true,
    subtree: true
  });

  // 外部ウィジェットの初期化関数
  function initializeExternalWidget(element) {
    console.log('外部ウィジェットを初期化します');
    // 必要な初期化処理を実行
  }

  // クリーンアップ
  window.addEventListener('beforeunload', () => {
    observer.disconnect();
  });
</script>

このアプローチの利点は、自分が制御していないスクリプトが何をしたのかを検知して、それに対応できるという点です。

実務では、以下のような対応が必要になります。

  • プラグインが追加した要素にカスタムスタイルを適用
  • GTMやアナリティクスが変更したデータ属性を監視
  • ブラウザ拡張機能が追加したコンテンツに対応
  • 予期しないDOM変更をログに記録してデバッグ

MutationObserverはこうした「予測不可能な外部変更への対応」に特に有効です。

国内シェアNo.1のエックスサーバーが提供するVPSサーバー『XServer VPS』

MutationObserverと他手法との比較

setIntervalポーリングとの違いとMutationObserverの優位性

DOM変更を検知する方法として、従来から使われてきたsetIntervalによるポーリングとMutationObserverを比較することは重要です。単に「MutationObserverの方が高速」と言うだけではなく、仕組みから理解する必要があります。

ポーリング(polling)とは

クライアント(ブラウザなど)がサーバーに対して、一定の間隔で「新しいデータはありますか?」と定期的に問い合わせを行う通信方式のこと。よくある使われ方としてはチャットやメッセージアプリの新着メッセージ取得などがある

setIntervalでのポーリングの仕組み

// 従来のsetIntervalを使ったポーリング
setInterval(() => {
  // 定期的に(例:100msごとに)DOMをチェック
  const currentCount = document.querySelectorAll('.item').length;

  if (currentCount !== previousCount) {
    console.log('要素数が変わりました');
    previousCount = currentCount;
  }
}, 100);

このアプローチの問題点

  1. 不要なチェックが発生する:100msごとに実行されるため、実際にDOM変更が起きていなくてもチェック処理が走ります。ブラウザのリソースを無駄に消費します
  2. 検知の遅延:セット間隔によって遅延が発生します。100msの間隔なら、最大100ms遅延します
  3. 精度の問題:複数の変更が同じ間隔内に起きた場合、1つにまとめられて見えることもあります
  4. CPU負荷:定期的にDOMを走査するため、ページが重くなるにつれて影響が大きくなります

MutationObserverの仕組み

// MutationObserverを使った監視
const observer = new MutationObserver((mutations) => {
  // DOM変更が実際に発生した時点でだけ実行
  console.log('DOM変更が検知されました');
  mutations.forEach((mutation) => {
    if (mutation.type === 'childList') {
      console.log('要素が追加・削除されました');
    }
  });
});

observer.observe(container, {
  childList: true
});

MutationObserverはブラウザのレンダリング機構に統合されているため、実際にDOM変更が発生したときだけコールバック関数が実行されます。不要なチェック処理は発生しません。

具体的な性能比較

実際のコード例で動作の違いを見てみましょう。

<div id="container"></div>

<button id="add-item-btn">要素を1000個追加</button>
<div id="results"></div>

<script>
  const resultsDiv = document.getElementById('results');
  const container = document.getElementById('container');

  // setIntervalによるポーリング
  let pollingCheckCount = 0;
  let pollingDetectCount = 0;

  const pollingInterval = setInterval(() => {
    pollingCheckCount++;
    const currentCount = container.children.length;
    if (currentCount > 0 && currentCount !== lastCount) {
      pollingDetectCount++;
      lastCount = currentCount;
    }
  }, 50);

  // MutationObserverによる監視
  let mutationDetectCount = 0;

  const observer = new MutationObserver((mutations) => {
    mutationDetectCount++;
  });

  observer.observe(container, {
    childList: true
  });

  let lastCount = 0;

  // 要素を1000個追加
  document.getElementById('add-item-btn').addEventListener('click', () => {
    const startTime = performance.now();

    for (let i = 0; i < 1000; i++) {
      const div = document.createElement('div');
      div.textContent = `アイテム ${i + 1}`;
      container.appendChild(div);
    }

    const endTime = performance.now();

    // 結果を表示
    resultsDiv.innerHTML = `
      <p>処理時間: ${(endTime - startTime).toFixed(2)}ms</p>
      <p>setInterval チェック回数: ${pollingCheckCount}</p>
      <p>setInterval 検知回数: ${pollingDetectCount}</p>
      <p>MutationObserver 検知回数: ${mutationDetectCount}</p>
    `;

    // 後処理
    clearInterval(pollingInterval);
    observer.disconnect();
  });
</script>

このコードを実行すると、以下のことが分かります。

  • setIntervalは多くの「チェック」を行いますが、その多くはDOM変更がない場合のムダなチェックです
  • MutationObserverは検知回数が少なく(変更が1000回あれば、複数の変更が1つのコールバック呼び出しでまとめられることもあります)、ムダなチェックがありません

ブラウザへの負荷の違い

  • setInterval ポーリング:定期的にDOMを走査するため、ページが複雑になると顕著にパフォーマンスが低下します
  • MutationObserver:ブラウザがDOM変更を検知する際に組み込まれた機構を利用するため、ブラウザ自体が効率的に処理できます

いつsetIntervalが適切か

ただし、setIntervalが完全に不要というわけではありません。以下のような場面ではsetIntervalの方が適切です。

  • 定期的に「状態の確認」が必要な場合:「毎秒、サーバーのステータスを確認する」といった用途。DOM変更ではなく、状態チェックが目的
  • シンプルな実装が優先される場合:MutationObserverの学習コストが不要な場合
  • ブラウザ互換性が重要な場合:極めて古いブラウザ対応が必須(実装では、setIntervalはブラウザサポートがより広い)

ただ、現在のWebブラウザではMutationObserverは十分にサポートされているため、DOM変更の監視目的ではMutationObserverを選ぶべきです。

通常イベント(click/input)で十分なケースとの境界線

MutationObserverは便利ですが、すべてのDOM監視に必要というわけではありません。通常のイベントリスナー(clickinputchangeなど)で対応できるケースがあります。どう使い分けるべきかを整理しましょう。

ユーザー操作が明確な場合は通常イベントを使う

// ❌ MutationObserverを使う必要がない例
const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
      console.log('入力値が変わりました');
    }
  });
});

textInput.observe(textInput, {
  attributes: true,
  attributeFilter: ['value']
});

// ✅ 通常イベントで十分
textInput.addEventListener('input', (event) => {
  console.log('入力値が変わりました:', event.target.value);
});

inputイベントは、テキストボックスへのユーザー入力によって発火します。MutationObserverで属性変更を監視する必要はありません。

同様に、以下のケースでは通常イベントを優先してください。

  • ボタンのクリック → clickイベント
  • フォーム送信 → submitイベント
  • チェックボックスの変更 → changeイベント
  • キーボード入力 → keydownkeyupイベント
  • フォーカスの移動 → focusblurイベント

外部スクリプトによる変更が不確定な場合はMutationObserverを使う

// ✅ MutationObserverが必要な例
// WordPress プラグインやGTMが予測不可能なタイミングで変更する場合
const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
      console.log('外部スクリプトがclass属性を変更しました');
    }
  });
});

element.observe(element, {
  attributes: true,
  attributeFilter: ['class']
});

境界線:イベントデリゲーション

動的に追加される要素へのイベント処理では、以下のように判断します。

// ケース1:イベントデリゲーションで十分
// 動的に追加されるボタンのクリックを処理したい
const container = document.getElementById('container');

container.addEventListener('click', (event) => {
  if (event.target.classList.contains('action-btn')) {
    console.log('ボタンがクリックされました');
  }
});

// 新しいボタンを追加しても、自動的に上記のイベントハンドラが対応

// ケース2:MutationObserverが必要
// 動的に追加された要素に対して「クリック以外の複雑な処理」が必要
const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    mutation.addedNodes.forEach((node) => {
      if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('special-btn')) {
        // クリック以外の初期化処理が必要
        node.addEventListener('focus', handleFocus);
        node.addEventListener('blur', handleBlur);
        initializeTooltip(node);
      }
    });
  });
});

observer.observe(container, {
  childList: true,
  subtree: true
});

判断基準の整理

以下のフローチャートで判断します。

  1. ユーザーの明確な操作か?
    • Yes → 通常イベント(clickinputなど)を使う
    • No → 次へ
  2. 要素の追加・削除のみの処理か?
    • Yes → イベントデリゲーション検討
    • No → 次へ
  3. DOM変更の検知そのものが必要か?
    • Yes → MutationObserverを使う
    • No → 他の方法を検討

最適なDOM監視手法を選ぶための判断基準

様々なDOM監視手法がある中で、状況に応じて最適な方法を選ぶための判断基準をまとめます。

目的別の推奨手法

目的推奨手法理由
ユーザーがボタンをクリックclickイベントユーザー操作は明確なイベントが存在
テキストボックスへの入力inputchangeイベントフォーム要素は専用イベントが最適
動的要素へのクリック処理イベントデリゲーション実装がシンプルで効率的
要素が表示領域に入ったか検知Intersection Observer表示判定専用のAPI
Ajaxで追加されたHTMLの処理MutationObserver外部スクリプトとの組み合わせに対応
外部スクリプトのDOM変更に対応MutationObserver予測不可能な変更に対応
定期的な状態確認setInterval / RequestAnimationFrame状態チェックが目的

実装の複雑さと信頼性

MutationObserverは強力ですが、以下の点に注意が必要です。

  • 実装がやや複雑(コールバックの管理、disconnect()の必要性)
  • 監視範囲を限定しないとパフォーマンス問題が起きやすい
  • デバッグが難しい(いつコールバックが実行されるかが予測しにくい)

一方、通常イベントやイベントデリゲーションは:

  • 実装がシンプル
  • 意図が明確
  • デバッグが容易

パフォーマンスの考慮

各手法のパフォーマンス特性を理解しましょう。

// 1. 通常イベント - 最も軽量
element.addEventListener('click', handleClick);  // リスナー追加コストのみ

// 2. イベントデリゲーション - 通常イベント並み
parent.addEventListener('click', (event) => {
  if (event.target.matches('.child-selector')) {
    // 処理
  }
});

// 3. setInterval ポーリング - CPU使用率が高い
setInterval(() => {
  // DOMをチェック(常に実行)
}, 100);

// 4. MutationObserver - 変更に応じた処理(適切に設定すれば軽量)
const observer = new MutationObserver(callback);
observer.observe(element, {
  childList: true,
  attributeFilter: ['class']  // 監視範囲を限定
});

// 5. Intersection Observer - 表示判定に最適化
const intersectionObserver = new IntersectionObserver(callback);
intersectionObserver.observe(element);

ハイブリッドアプローチ

実務では複数の手法を組み合わせることが多いです。

// 例:無限スクロール+遅延ロード
const content = document.getElementById('content');

// 1. Intersection Observer: スクロール位置を監視して次のページ読み込み指示
const intersectionObserver = new IntersectionObserver((entries) => {
  if (entries[0].isIntersecting) {
    loadNextPage();  // Fetch
  }
});

// 2. MutationObserver: 追加されたコンテンツを監視して初期化
const mutationObserver = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    mutation.addedNodes.forEach((node) => {
      if (node.nodeType === Node.ELEMENT_NODE) {
        // 画像の遅延ロード設定など
        initializeNewContent(node);
      }
    });
  });
});

// 3. 通常イベント: ユーザーのクリックなど明確な操作
content.addEventListener('click', (event) => {
  if (event.target.matches('.item-link')) {
    handleItemClick(event.target);
  }
});

// 複数の手法を組み合わせることで、堅牢な実装ができる

選択のチェックリスト

最適な手法を選ぶ際に、以下のチェックリストを使ってください。

  • ユーザーの明確な操作が発生するか? → Yes なら通常イベント
  • 新しく追加される要素が確定しているか? → Yes なら親要素でイベントデリゲーション
  • 要素の表示判定が必要か? → Yes なら Intersection Observer
  • 外部スクリプトによる予測不可能なDOM変更があるか? → Yes なら MutationObserver
  • 定期的な状態確認が目的か? → Yes なら setInterval(ただ、多くの場合は不要)

この判断フローに従うことで、パフォーマンスと保守性のバランスが取れた実装ができます。

MutationObserverを安全に使うための注意点

MutationObserverは強力なツールですが、使い方によってはパフォーマンス問題やメモリリークを引き起こします。実装時に注意すべき点を具体例と共に解説します。

監視範囲を必要最小限に限定する

MutationObserverで監視する範囲が広いほど、コールバック関数が頻繁に呼び出されます。特にsubtree: trueを指定して配下全体を監視する場合、ページ全体で起きるすべてのDOM変更が検知される可能性があり、パフォーマンスに大きな影響を与えます。

悪い例:監視範囲が広すぎる

// ❌ ページ全体を監視 - 非常に危険
const observer = new MutationObserver((mutations) => {
  console.log(`${mutations.length}件のDOM変更を検知`);
  // ここで重い処理を実行すると、ページ全体が遅くなる
});

// document.bodyの全体を監視
observer.observe(document.body, {
  childList: true,
  subtree: true,
  attributes: true
});

このコードの問題点は何でしょうか。

  1. コールバックが頻繁に実行される:ページ内のあらゆるDOM変更(スクリプトによる追加、ブラウザ拡張機能による変更、GTMによる変更など)がすべて検知されます
  2. 不必要な変更も監視される:自分が関心のない変更も含まれるため、フィルタリングが複雑になります
  3. パフォーマンス低下:JavaScriptの実行が長く続き、ブラウザがレスポンシブでなくなります

実際の動作を見てみましょう。

<body>
  <div id="main">
    <p>メインコンテンツ</p>
  </div>
  <div id="sidebar">
    <p>サイドバー</p>
  </div>
  <script src="third-party.js"></script>
  <script src="analytics.js"></script>
</body>

<script>
  let callbackCount = 0;

  const badObserver = new MutationObserver((mutations) => {
    callbackCount++;
  });

  // ❌ 悪い実装
  badObserver.observe(document.body, {
    childList: true,
    subtree: true,
    attributes: true
  });

  // ページ読み込み直後に何度コールバックが実行されるか
  setTimeout(() => {
    console.log(`コールバック実行回数: ${callbackCount}回`);
    // 結果:100回以上実行されることも珍しくない
  }, 3000);
</script>

改善例:監視対象を限定する

// ✅ 必要な要素のみを監視
const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    console.log('関心のある要素が変更されました');
  });
});

// 特定の要素のみを監視
const mainContent = document.getElementById('main');
observer.observe(mainContent, {
  childList: true,
  subtree: false  // 直下の子要素のみ。配下全体は不要な場合はfalse
});

この改善により:

  1. コールバック呼び出しが少なくなる:関心のある要素の変更のみが検知されます
  2. フィルタリング不要:すべての変更が関心対象なので、追加の判定が不要
  3. パフォーマンス向上:JavaScriptの実行時間が短くなり、ブラウザが応答性を保ちます

subtree: trueを使う際の注意

subtree: trueは配下全体を監視する便利なオプションですが、慎重に使う必要があります。

// subtree: trueの問題例
const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    // コールバック内でDOM操作を行うと...
    if (mutation.type === 'childList') {
      // 新しい要素が追加されたので、別の場所に追加処理を実行
      const newDiv = document.createElement('div');
      const parent = document.getElementById('log-container');
      parent.appendChild(newDiv);
      // これはさらにMutationObserverのコールバックをトリガーする可能性がある
    }
  });
});

// subtree: trueを指定すると、ネストされた変更まで検知
const container = document.getElementById('container');
observer.observe(container, {
  childList: true,
  subtree: true  // ⚠️ 配下全体を監視
});

この問題は後ほど詳しく説明しますが、subtree: trueを使う場合は必ず必要性を確認してください。

監視対象ごとに適切なオプションを設定する

MutationObserverのオプションを必要以上に指定すると、不要なコールバック呼び出しが増えます。監視対象に応じて、最小限のオプションを設定しましょう。

悪い例:すべてのオプションを有効にする

// ❌ すべてを監視 - 不要なコールバック呼び出しが多い
const observer = new MutationObserver((mutations) => {
  console.log(mutations);
});

observer.observe(element, {
  childList: true,      // 子要素の追加・削除
  attributes: true,     // すべての属性変更
  characterData: true,  // テキスト内容の変更
  subtree: true,        // 配下全体
  attributeOldValue: true,        // 属性変更前の値
  characterDataOldValue: true     // テキスト変更前の値
});

このコードでは、テキスト内容の変更も監視しているため、ページ内のあらゆるテキスト変更がコールバックをトリガーします。不要です。

改善例1:子要素の追加のみ監視

// ✅ 子要素の追加・削除のみ監視
const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    if (mutation.type === 'childList') {
      console.log('子要素が追加・削除されました');
    }
  });
});

observer.observe(container, {
  childList: true
  // その他のオプションは指定しない
});

改善例2:特定の属性のみ監視

// ✅ classとdata-status属性の変更のみ監視
const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    if (mutation.attributeName === 'class') {
      console.log('classが変更されました');
    }
    if (mutation.attributeName === 'data-status') {
      console.log('data-statusが変更されました');
    }
  });
});

observer.observe(element, {
  attributes: true,
  attributeFilter: ['class', 'data-status']  // 必要な属性のみ指定
});

attributeFilterを使うことで、特定の属性のみを監視できます。これにより、不要なコールバック呼び出しを大幅に削減できます。

改善例3:属性変更前の値が必要な場合

// ✅ 必要なときだけ oldValue を取得
const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    if (mutation.attributeName === 'class') {
      const newClass = mutation.target.className;
      const oldClass = mutation.oldValue;
      console.log(`class: "${oldClass}" → "${newClass}"`);
    }
  });
});

observer.observe(element, {
  attributes: true,
  attributeFilter: ['class'],
  attributeOldValue: true  // 変更前の値が必要なときだけ指定
});

オプション設定のチェックリスト:

  • childList:子要素の追加・削除を監視するか? 必要ならtrue
  • attributes:属性変更を監視するか? 特定属性のみならattributeFilterで限定
  • characterData:テキスト内容の変更を監視するか? ほとんどの場合不要
  • subtree:本当に配下全体を監視する必要があるか? 特定の要素でいいならfalse
  • attributeOldValue:変更前の属性値が必要か? 必要なときだけtrue
  • characterDataOldValue:テキスト変更前の値が必要か? ほとんどの場合不要

disconnect()によるクリーンアップを徹底する

MutationObserverの監視を終了する際には、必ずdisconnect()を呼び出してください。呼び出さないと、メモリリークやコールバック関数の予期しない実行につながります。

メモリリークの例

// ❌ disconnect() を呼び出さない
function setupObserver() {
  const observer = new MutationObserver((mutations) => {
    console.log('DOM変更を検知');
  });

  const container = document.getElementById('container');
  observer.observe(container, {
    childList: true
  });

  // 監視処理を終了していない
  // ページを離脱するまで、メモリに留まる
  // コールバック関数も保持され続ける
}

// この関数を複数回呼び出すと、MutationObserverのインスタンスが増え続ける
setupObserver();
setupObserver();
setupObserver();

このコードでは、setupObserver()を複数回呼び出すたびにMutationObserverのインスタンスが増え、すべてがメモリに留まります。

改善例:適切にdisconnect()を呼び出す

// ✅ 監視を終了するときに disconnect() を呼び出す

class ComponentObserver {
  constructor(element) {
    this.element = element;
    this.observer = null;
  }

  setup() {
    this.observer = new MutationObserver((mutations) => {
      console.log('DOM変更を検知');
    });

    this.observer.observe(this.element, {
      childList: true
    });
  }

  destroy() {
    if (this.observer) {
      this.observer.disconnect();
      this.observer = null;
    }
  }
}

// コンポーネントのライフサイクルで管理
const component = new ComponentObserver(document.getElementById('container'));
component.setup();

// コンポーネントが不要になったときに destroy を呼ぶ
setTimeout(() => {
  component.destroy();  // disconnect() が呼ばれる
}, 5000);

SPA(Single Page Application)での注意

SPAでは、ページ遷移時にコンポーネントが削除されます。このとき、MutationObserverも必ず終了する必要があります。

// ✅ React での例
import React, { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    const observer = new MutationObserver((mutations) => {
      console.log('DOM変更を検知');
    });

    const container = document.getElementById('container');
    observer.observe(container, {
      childList: true
    });

    // クリーンアップ関数で disconnect() を呼ぶ
    return () => {
      observer.disconnect();
    };
  }, []);

  return <div id="container">コンテンツ</div>;
}

export default MyComponent;
// ✅ Vue での例
export default {
  data() {
    return {
      observer: null
    };
  },
  mounted() {
    this.observer = new MutationObserver((mutations) => {
      console.log('DOM変更を検知');
    });

    this.observer.observe(this.$refs.container, {
      childList: true
    });
  },
  beforeUnmount() {
    // コンポーネント破棄時に disconnect()
    if (this.observer) {
      this.observer.disconnect();
    }
  }
};

MutationObserver自身がDOM変更をトリガーする問題

コールバック内でDOM操作を行うと、そのDOM変更がMutationObserverで検知され、再びコールバックが呼ばれる可能性があります。これが無限ループになることもあります。

// ⚠️ 注意が必要な例
const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    if (mutation.type === 'childList') {
      // コールバック内でDOM操作
      const newElement = document.createElement('div');
      mutation.target.appendChild(newElement);
      // この appendChild がさらにコールバックをトリガーする可能性
    }
  });
});

observer.observe(container, {
  childList: true,
  subtree: true
});

この問題を避けるために:

  1. コールバック内のDOM操作を最小限にする
  2. 必要な場合は disconnect() → DOM操作 → observe() の流れにする
  3. フラグで再帰を防ぐ
// ✅ 再帰を防ぐ実装
let isUpdating = false;

const observer = new MutationObserver((mutations) => {
  if (isUpdating) return;  // 再帰を防ぐ

  isUpdating = true;
  try {
    mutations.forEach((mutation) => {
      if (mutation.type === 'childList') {
        console.log('要素が追加されました');
        // ここでのDOM操作は再びコールバックをトリガーしない
      }
    });
  } finally {
    isUpdating = false;
  }
});

observer.observe(container, {
  childList: true
});

別の方法として、監視を一時的に停止する手法もあります。

// ✅ 監視を一時的に停止
const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    if (mutation.type === 'childList') {
      // 監視を停止
      observer.disconnect();

      // DOM操作を実行
      const newElement = document.createElement('div');
      mutation.target.appendChild(newElement);

      // 監視を再開
      observer.observe(container, {
        childList: true
      });
    }
  });
});

observer.observe(container, {
  childList: true
});

ただし、この方法は監視の間に他の変更を見落とす可能性があるため、フラグを使う方法の方が推奨されます。

実装時のチェックリスト

MutationObserverを実装するときは、以下を確認してください。

  • 監視対象を必要最小限に限定しているか
  • 必要なオプションのみを指定しているか
  • attributeFilterで属性を限定しているか
  • subtree: trueは本当に必要か
  • ページ遷移やコンポーネント破棄時にdisconnect()を呼んでいるか
  • コールバック内でのDOM操作で無限ループが起きないか
  • メモリ使用量が増え続けていないか(開発者ツールで確認)
◆◇◆ 【衝撃価格】VPS512MBプラン!1時間1.3円【ConoHa】 ◆◇◆

よくある質問(FAQ)

MutationObserverとは何ですか?

MutationObserverは、ウェブページのHTML要素(DOM)の追加・削除・属性変更をリアルタイムに検知し、その変更が発生したときに指定した関数を自動的に実行するJavaScriptの標準APIです。ブラウザに組み込まれた機構を利用するため、setIntervalのような定期的なチェック処理よりも効率的です。

動的に追加されたボタンへのクリックイベント処理には必ずMutationObserverが必要ですか?

いいえ。イベントデリゲーションを使える場合は、MutationObserverは不要です。親要素にclickイベントリスナーを設定し、クリック対象を確認する方法が簡潔で効率的です。MutationObserverが必要なのは、動的要素に複雑な初期化処理が必要な場合や、イベント以外の処理を行う場合です。

MutationObserverでパフォーマンス問題が起きた場合、どう対処しますか?

以下の点を確認してください。(1)監視範囲:document.body全体など広すぎないか。特定の要素のみを監視するように変更する。(2)監視オプション:不要なオプションを有効にしていないか。attributes: trueを指定しているなら、attributeFilterで特定属性のみに限定する。(3)コールバック内の処理:重い処理を実行していないか。必要に応じて非同期処理やrequestAnimationFrameを使う。(4)subtree: true:本当に配下全体を監視する必要があるか確認する。

MutationObserverとsetIntervalではどちらを使うべきですか?

DOM変更を検知したい場合はMutationObserver、定期的に状態を確認したい場合はsetIntervalです。MutationObserverはDOM変更が発生したときだけコールバックが実行されるため、不要なチェック処理がなく効率的です。setIntervalは定期的にコードを実行する必要があるユースケース(例:サーバーのステータスを毎秒確認)に向いています。

MutationObserverのコールバック内でDOM操作を行うと何が起きますか?

DOM操作がMutationObserverで検知され、再びコールバック関数が呼ばれる可能性があります。無限ループを防ぐには、フラグを使って再帰を防止する方法(isUpdatingフラグなど)か、disconnect() → DOM操作 → observe()の流れを使う方法があります。コールバック内の大規模なDOM操作は避け、必要最小限に留めてください。

MutationObserverは監視を一度停止して再開できますか?

はい。disconnect()で監視を停止します。ただし、再度監視を開始する場合は新しいMutationObserverインスタンスを作成してobserve()を呼び出す必要があります。既存のインスタンスで再度observe()を呼び出すことも可能ですが、新規インスタンスを作成する方法が一般的です。重要なのは、不要になった監視は必ずdisconnect()で終了してメモリリークを防ぐことです。

MutationObserverでclass属性の変更だけを監視できますか?

はい。attributeFilterオプションを使います。

例:observer.observe(element, { attributes: true, attributeFilter: [‘class’] })。

こうすることで、class属性の変更のみを監視し、他の属性変更は検知しません。結果として、不要なコールバック呼び出しを減らし、パフォーマンスを向上させることができます。

MutationObserverとIntersectionObserverは何が違いますか?

MutationObserverはDOM要素の追加・削除・属性変更を検知するのに対し、IntersectionObserverは要素がビューポート(表示領域)に入ったか出たかを検知するAPIです。無限スクロール機能では、IntersectionObserverでスクロール位置を監視し、次のコンテンツ読み込みをトリガーし、その後MutationObserverで追加されたコンテンツを検知するという使い分けをします。

MutationObserverはブラウザ互換性が問題になりますか?

いいえ。MutationObserverは全ての現代的なブラウザ(Chrome、Firefox、Safari、Edge)で十分にサポートされています。Internet Explorer 11以下への対応が必要な場合のみ、setIntervalでのポーリングなど代替案を検討してください。ただし、Internet Explorerのサポートはマイクロソフト側で既に終了しているため、新規プロジェクトではMutationObserverを使用して問題ありません。

複数の要素を監視する場合、1つのMutationObserverで複数の要素を見てもいいですか?

はい。1つのMutationObserverで複数の要素を監視できます。observe()メソッドを複数回呼び出すことで実現します。ただし、すべての変更が1つのコールバック関数に渡されるため、フィルタリングが複雑になる可能性があります。監視対象の要素数が多い場合や、各要素で異なる処理が必要な場合は、要素ごとに別のMutationObserverインスタンスを作成する方が保守性が高いです。

Audibleプレミアムプラン30日間無料体験

まとめ

MutationObserverは、ウェブ開発で動的なDOM変更に対応するための不可欠なツールです。この記事を通じて、基本から実践的な使い方、そして注意点まで学んできました。最後に、重要なポイントを整理します。

MutationObserverの役割

MutationObserverの最大の役割は、外部スクリプトやユーザー操作による予測不可能なDOM変更に対応することです。特にWordPressプラグイン、Google Tag Manager、ブラウザ拡張機能など、制御できないスクリプトが加わる環境では、MutationObserverが活躍します。従来のポーリング方式(setInterval)と異なり、変更が発生したときだけ処理を実行するため、パフォーマンスも優れています。

実装時の判断基準を忘れずに

記事で何度も述べたように、すべてのDOM監視にMutationObserverが必要というわけではありません。実装前に必ず確認すべきことは以下の通りです。

  • ユーザーの明確な操作(クリック、入力)が発生するなら、通常のイベントリスナーを使う
  • 新規要素への単純なイベント設定なら、イベントデリゲーションを検討する
  • 要素の表示判定が必要なら、Intersection Observerを活用する
  • 定期的な状態確認が目的なら、setIntervalやRequestAnimationFrameの方が適切な場合もある

MutationObserverは「やはり必要」と判断したときに使うべきツールです。

安全な実装の三大原則

MutationObserverを実装する際は、以下の3つを絶対に守ってください。

  1. 監視範囲を限定するdocument.body全体など、広すぎる範囲の監視は避け、特定の要素に限定する
  2. オプションを最小化する:必要なオプションのみを指定し、特にattributeFilterを活用して不要な監視を削減する
  3. disconnect()でクリーンアップする:ページ遷移やコンポーネント破棄時に必ず監視を終了し、メモリリークを防ぐ

この3つを守れば、パフォーマンス問題や予期しない動作の大半は防げます。

実装パターンの整理

この記事で扱った主なユースケースをまとめます。

  • Ajax/Fetch対応:外部からのコンテンツ読み込みに対応する必要があれば、MutationObserverで追加要素を検知
  • イベント設定:イベントデリゲーションが使えないなら、MutationObserverで新規要素を検知してイベント設定
  • 属性監視:class変更やデータ属性変更に対応する場合、attributeFilterで効率化
  • 無限スクロール:Intersection ObserverとMutationObserverを組み合わせて実装
  • 外部スクリプト対応:WordPress、GTM、ブラウザ拡張機能など、予測不可能な変更に対応

これらのパターンを理解していれば、実務での応用は容易です。

MutationRecordを味方にする

コールバック関数に渡されるmutations配列の各要素(MutationRecord)には、DOM変更の詳細情報が詰まっています。typetargetaddedNodesremovedNodesattributeNameoldValueなど、これらのプロパティを効果的に使うことで、より正確で効率的な実装ができます。特にattributeOldValueを活用すれば、変更前後の値を比較した処理が可能です。

デバッグのコツ

MutationObserverが思ったように動作しない場合は、以下を確認してください。

  • コンソールにログを出力し、コールバックが実行されているか確認
  • mutationsの内容を確認して、期待した変更が検知されているか確認
  • 開発者ツールのパフォーマンスタブで、コールバックの実行時間が長くないか確認
  • メモリ使用量が増え続けていないか確認(メモリリークの兆候)

また、複雑な実装の場合は、MutationObserverを一度停止して、その間に何が起きているかを調べるテクニックも有効です。

保守性を考えた実装

実務では、複数人のチームで開発することがほとんどです。MutationObserverを使う際は、以下の点に注意してコードを書くことで、保守性が格段に向上します。

  • コメントに「なぜこの要素を監視する必要があるのか」を記述
  • 監視範囲やオプションの理由を説明
  • disconnect()を呼び出すタイミングを明確にする
  • テストコードを書き、期待した動作を確認

「自分が書いたコードだから」ではなく、「他の開発者が6か月後に読んでも理解できる」ことを目指しましょう。

他のAPIとの組み合わせ

MutationObserverは単独で使うことも多いですが、実務では他のAPIと組み合わせることが重要です。

  • Intersection Observer:スクロール位置 + DOM変更検知
  • ResizeObserver:要素サイズ変更 + DOM変更検知
  • イベントリスナー:ユーザー操作 + DOM変更検知
  • Fetch API:非同期データ取得 + DOM追加検知

これらを柔軟に組み合わせることで、複雑なインタラクティブなページにも対応できます。

最後に

MutationObserverは「必ず必要」なAPIではありませんが、モダンなWebアプリケーションでは活躍の場がたくさんあります。正しく理解し、適切に使うことで、ユーザーにより良い体験を提供できます。

記事で紹介したコード例を実際に試してみることをお勧めします。ブラウザの開発者ツールを開き、コードを実行してコンソールを確認することで、MutationObserverの動作がより深く理解できるはずです。

◆◇◆ 【衝撃価格】VPS512MBプラン!1時間1.3円【ConoHa】 ◆◇◆
タイトルとURLをコピーしました