【JavaScript】MutationObserverでDOM変化を監視する方法|属性・子要素・非同期バッチ・無限ループ対策まで

JavaScript

外部ライブラリやJavaScriptによってDOMが書き換えられたタイミングで処理を実行したいとき、MutationObserver を使うとDOMの変化を効率的に監視できます。要素の追加・削除、属性の変更、テキストの変更を検知できる標準APIです。この記事では基本から、非同期で動く挙動や無限ループの落とし穴まで実務目線で解説します。

この記事でわかること

  • 基本構文(new MutationObserverobserve)と監視オプション
  • コールバックが非同期でまとめて呼ばれる挙動
  • attributeOldValue で変更前の値を取得する
  • disconnect / takeRecords による停止とフラッシュ
  • 自己トリガーによる無限ループの防ぎ方
  • Event Delegation との使い分けと Observer 三兄弟の比較
結論:new MutationObserver(callback) を作り、observe(target, options) で監視を開始します。コールバックは同期的には呼ばれず、現在の処理が終わってからまとめて実行されます。不要になったら必ず disconnect() し、コールバック内で監視対象を変更するときは無限ループに注意します。
スポンサーリンク

MutationObserverとは

MutationObserver はDOMの変更を監視し、変化があったときにコールバックを実行するAPIです。従来の DOMSubtreeModified などの Mutation イベントは非推奨となり、現在は MutationObserver が標準的な方法です。「自分が直接書き換えていないDOMの変化(サードパーティスクリプトや外部ライブラリによる変更)」に反応したい場面で特に役立ちます。

基本構文

監視対象の要素、コールバック、監視オプションの3つを用意します。

JavaScript:基本構文
// 監視対象の要素
const target = document.getElementById("content");

// 変化があったときに呼ばれるコールバック
const callback = (mutationsList, observer) => {
  for (const mutation of mutationsList) {
    if (mutation.type === "childList") {
      console.log("子要素の追加・削除が検出されました");
    }
    if (mutation.type === "attributes") {
      console.log("属性が変更されました:", mutation.attributeName);
    }
  }
};

const observer = new MutationObserver(callback);

// オプションを指定して監視開始
observer.observe(target, {
  childList: true,   // 子要素の追加・削除
  attributes: true,  // 属性の変更
  subtree: true,     // 子孫まで対象に含める
});

監視オプション

observe の第2引数で監視内容を細かく制御できます。childList / attributes / characterData少なくとも1つを true にする必要があります

  • childList:子要素の追加・削除を監視
  • attributes:属性の変更を監視
  • characterData:テキストノードの変更を監視
  • subtree:子孫ノードまで監視対象に含める
  • attributeFilter:特定の属性だけ監視(配列で指定)
  • attributeOldValue / characterDataOldValue変更前の値も記録する

【重要】コールバックは非同期でまとめて呼ばれる

見落としやすいポイントです。MutationObserver のコールバックは変更のたびに同期的に呼ばれるのではなく、現在の処理が終わってから、溜まった変更をまとめて(マイクロタスクとして)実行されます。

JavaScript:実行順で挙動を確認
const target = document.getElementById("box");
const observer = new MutationObserver(() => console.log("変更検出"));
observer.observe(target, { childList: true });

target.appendChild(document.createElement("p"));
target.appendChild(document.createElement("p"));
console.log("同期処理おわり");

// 出力:
// 同期処理おわり   ← 先に出る
// 変更検出         ← 2回分の変更がまとまって、後から1回
「すぐ発火しない」のは仕様
同期コードの直後に結果を見ようとすると「動いていない」と勘違いしがちですが、コールバックは現在のタスク完了後に呼ばれます。複数の変更が1回のコールバックにバッチ(まとめて)渡されるため、効率も良くなります。

実用例:動的に追加された要素を検知する

JavaScriptで動的に追加される要素を検知する例です。addedNodes を走査し、要素ノード(nodeType === 1)だけを対象にします。

JavaScript:追加されたリスト項目を検知
const list = document.getElementById("list");

const observer = new MutationObserver((mutations) => {
  for (const mutation of mutations) {
    for (const node of mutation.addedNodes) {
      // 要素ノードで li のものだけ処理
      if (node.nodeType === 1 && node.matches("li")) {
        console.log("新しいリスト項目:", node.textContent);
      }
    }
  }
});

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

実用例:属性変化と「変更前の値」

属性の変化を監視し、attributeOldValue変更前の値も取得する例です。attributeFilter で対象属性を絞るとノイズを減らせます。

JavaScript:class 属性の変化を旧値つきで監視
const box = document.querySelector(".box");

const observer = new MutationObserver((mutations) => {
  for (const m of mutations) {
    if (m.type === "attributes") {
      const newValue = box.getAttribute(m.attributeName);
      console.log(`${m.attributeName}: ${m.oldValue} → ${newValue}`);
    }
  }
});

observer.observe(box, {
  attributes: true,
  attributeOldValue: true,     // 変更前の値を m.oldValue に記録
  attributeFilter: ["class"],  // class 属性だけ監視
});

監視の停止(disconnect)と takeRecords

不要になったら disconnect() で監視を止めます(メモリリーク防止に重要)。まだコールバックに渡されていない保留中の変更がある場合は、takeRecords() で取り出してキューを空にできます。

JavaScript:停止と保留分のフラッシュ
// 保留中の変更を取り出して処理(disconnect 直前のフラッシュなどに)
const pending = observer.takeRecords();
if (pending.length) {
  // pending を処理...
}

observer.disconnect(); // 監視を停止

【重要】自己トリガーによる無限ループを防ぐ

コールバック内で監視対象を変更すると無限ループしうる
コールバックの中で監視中のDOMをさらに変更すると、その変更がまた検知されて再発火し、ループに陥ることがあります。対処は、変更の前に一時停止して変更後に再開するか、フラグでガードして自分の変更を無視します。
JavaScript:一時停止して自分の変更を除外する
const observer = new MutationObserver(() => {
  observer.disconnect();                 // いったん監視を止める
  target.appendChild(document.createElement("div")); // 自分でDOMを変更
  observer.observe(target, { childList: true }); // 監視を再開
});

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

パフォーマンスの注意

監視範囲は最小に
subtree: true を広いDOMにかけると、変更のたびに大量の MutationRecord が生成され重くなります。必要な要素だけを最小スコープで監視し、attributeFilter で対象を絞り、不要になったら必ず disconnect() しましょう。

Event Delegation との使い分けと Observer の比較

「動的要素にイベントを付けるだけ」なら Event Delegation
動的に追加される要素にクリックなどのイベントを付けたいだけなら、MutationObserver より Event Delegation(イベント委譲)の方が簡単で軽量です。MutationObserver自分が制御していないDOMの変化そのものに反応したいときに使います。

監視系のAPI(Observer三兄弟)は、監視する対象が異なります。

API 監視するもの 主な用途
MutationObserver DOMの変更(要素・属性・テキスト) 制御外のDOM変化に反応
IntersectionObserver 要素の表示(ビューポートとの交差) 遅延読み込み・スクロール演出
ResizeObserver 要素のサイズ変化 レスポンシブな再計算

それぞれの実装はIntersectionObserverで要素をふわっと表示ResizeObserverで要素の高さを揃える、フォーム入力のリアルタイム監視はフォーム入力をリアルタイムで監視する方法、動的なDOM操作はマウスオーバーで要素を動的に追加・削除する実践ガイドを参照してください。

よくある質問(FAQ)

QMutationObserverとは何ですか?
ADOMの変更(要素の追加・削除・属性変更・テキスト変更)を監視するAPIです。サードパーティスクリプトや非同期処理で変更されるDOMへの対応、動的コンテンツのフック処理に使われます。
Qコールバックがすぐ呼ばれません。
A仕様です。MutationObserver のコールバックは同期的には呼ばれず、現在の処理が終わってから、溜まった変更をまとめて実行されます。同期コードの直後に結果を見ようとすると動いていないように見えますが、直後のタイミングで呼ばれます。
Q変更前の値を取得するには?
AattributeOldValue: true(属性)または characterDataOldValue: true(テキスト)をオプションに指定すると、コールバックの mutation.oldValue で変更前の値を取得できます。
Q監視を停止するには?
Aobserver.disconnect() で停止します。監視対象が不要になったら必ず停止してメモリリークを防いでください。保留中の変更は observer.takeRecords() で取り出せます。
Qコールバック内でDOMを変更したらループしました。
A監視中のDOMをコールバック内で変更すると、それが再び検知されて無限ループになります。変更の前に disconnect() し、変更後に observe() で再開するか、フラグでガードしてください。

まとめ

  • 基本new MutationObserverobserve(target, options)
  • 非同期:コールバックは現在のタスク後にまとめて呼ばれる
  • 旧値attributeOldValue / characterDataOldValue
  • 停止disconnect()(+ takeRecords() でフラッシュ)
  • 無限ループ:コールバック内の変更は一時停止やフラグでガード
  • 使い分け:イベント付与だけなら Event Delegation が簡単

非同期でバッチ実行される挙動と無限ループの対策を押さえれば、制御外のDOM変化にも安全に反応できる堅牢なフロントエンドを実装できます。