2026年6月3日
2026年6月3日
WordPressでMutationObserverを使ってDOMの変更を監視する実装例
MutationObserver は、要素の追加・削除や属性の変化を検知して、決まった処理を後から走らせるためのブラウザ API です。WordPress では、Ajax の「もっと見る」で追加された記事や、他のプラグインが後から差し込む要素に、自分のテーマの処理を当てたいときに使えます。ただし、使い方を誤るとページ全体を重くしたり、無限ループを起こしたりします。この記事では、まず「本当に MutationObserver が必要か」を判断する基準を示し、そのうえで後から追加される表に横スクロールの枠を付ける例と、要素の出現を待つ関数の2つを実装し、性能とデバッグの注意点を説明します。
MutationObserver を使う前に、イベントやフックが無いか探す
DOM の変化を監視するのは、ほかに手段がないときの方法です。変化を起こしている側がイベントやフックを用意していれば、そちらを使うほうが軽く、意図も明確になります。WordPress まわりでは、たとえば次のようなものがあります。
- Contact Form 7:送信完了時などに
wpcf7mailsentなどの DOM イベントを発行します。完了メッセージの要素を監視する必要はありません。 - WooCommerce:カート追加時などに jQuery のカスタムイベント(
added_to_cartなど)を発行します。 - ブロックエディタ:エディタの状態は
@wordpress/data(wp.data.subscribe()やselect())で取得します。近年のバージョンでは編集画面のキャンバスが iframe で描画されることがあり、DOM の監視は壊れやすいため避けてください。 - 自作テーマの Ajax 処理:読み込み完了時に自分で
CustomEventを発行すれば、監視は要りません。
イベント名や発行のタイミングはプラグインのバージョンで変わることがあるため、使う前に各プラグインの公式ドキュメントで確認してください(2026年10月現在の情報です)。こうした手段が無い、他者のコードが要素を差し込んでくる場面で、MutationObserver を使います。
監視の基本:何を、どの範囲で見るか
observe() の第2引数で、監視する変化の種類を指定します。
childList: true:子要素の追加・削除subtree: true:子孫要素まで範囲を広げるattributes: true:属性の変化(attributeFilterで属性名を絞れます)characterData: true:テキストノードの内容の変化attributeOldValue/characterDataOldValue:変化前の値も記録する
性能への影響は、監視の範囲と種類でほぼ決まります。document.body に subtree と attributes を同時に指定すると、スライダーやアニメーションが style 属性を書き換えるたびにコールバックが呼ばれます。監視する要素はできるだけ狭く、種類は必要なものだけに絞るのが原則です。
実装例1:後から追加された表に横スクロールの枠を付ける
スマートフォンで横に長い表がはみ出す問題への対策として、表を横スクロール可能な枠で包む処理を考えます。ページ読み込み時にある表だけでなく、「もっと見る」で追加された記事の表にも適用したい、という想定です。
なお、コアの表ブロック(.wp-block-table)は、標準のスタイルで横スクロールに対応しています。ここでは、クラシックエディタで書いた記事やプラグインが出力する表など、ブロック以外の表を対象にします。
スクリプトの読み込み
子テーマの functions.php に追加します。作業前にバックアップを取り、ステージング環境で確認してください。
<?php
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_script(
'my-table-scroll',
get_stylesheet_directory_uri() . '/js/table-scroll.js',
array(),
'1.0.0',
array( 'strategy' => 'defer', 'in_footer' => true )
);
wp_add_inline_style( 'wp-block-library', '.my-table-scroll{overflow-x:auto;max-width:100%}' );
} );
strategy 引数は WordPress 6.3 以降の書き方です。wp_add_inline_style() は、すでに登録されているスタイル(ここではブロックの基本スタイル)の後ろに CSS を追加します。ブロックの基本スタイルを読み込まないテーマでは、自分のテーマのスタイルのハンドル名に置き換えてください。
JavaScript
( function () {
const root = document.querySelector( 'main' ) || document.body;
function wrapTables( scope ) {
if ( scope.nodeType !== Node.ELEMENT_NODE ) {
return;
}
const tables = scope.matches( 'table' ) ? [ scope ] : Array.from( scope.querySelectorAll( 'table' ) );
for ( const table of tables ) {
// 処理済み、またはコアの表ブロック内なら何もしない
if ( table.dataset.myWrapped || table.closest( '.wp-block-table' ) ) {
continue;
}
table.dataset.myWrapped = '1';
const wrap = document.createElement( 'div' );
wrap.className = 'my-table-scroll';
wrap.tabIndex = 0; // キーボードでもスクロールできるようにする
wrap.setAttribute( 'role', 'region' );
wrap.setAttribute( 'aria-label', '表(横にスクロールできます)' );
table.parentNode.insertBefore( wrap, table );
wrap.appendChild( table );
}
}
// 読み込み時点の表
wrapTables( root );
// 追加された要素をまとめて、次のフレームで処理する
let pending = [];
let scheduled = false;
const observer = new MutationObserver( ( records ) => {
for ( const record of records ) {
record.addedNodes.forEach( ( node ) => pending.push( node ) );
}
if ( ! scheduled && pending.length ) {
scheduled = true;
requestAnimationFrame( () => {
const nodes = pending;
pending = [];
scheduled = false;
nodes.forEach( ( node ) => {
if ( node.isConnected ) {
wrapTables( node );
}
} );
} );
}
} );
observer.observe( root, { childList: true, subtree: true } );
} )();
この実装で気をつけた点
無限ループを防ぐ目印:コールバックの中で DOM を変更すると、その変更自体がまた監視に引っかかります。この例では、表を枠で包む操作で新しい div が追加されるため、もう一度コールバックが呼ばれます。data-my-wrapped を付けて処理済みの表を飛ばすことで、同じ表を何度も包む事態を防いでいます。別の方法として、DOM を変更する間だけ observer.disconnect() し、終わってから observe() し直すやり方もあります。
まとめて処理する:MutationObserver のコールバックは、変化のたびではなく、まとまった変化の記録(MutationRecord の配列)を受け取ります。それでも、記事を何十件も追加する処理では大量の記録が届くため、追加されたノードを集めてから requestAnimationFrame で1回だけ処理しています。
既に取り除かれた要素を処理しない:集めたノードが処理までの間に削除されていることがあるため、isConnected で確認しています。
アクセシビリティ:スクロールする枠に tabIndex とラベルを付けると、キーボード利用者も矢印キーで横スクロールでき、スクリーンリーダーにも領域の意味が伝わります。
実装例2:要素が現れるのを待つ関数
他のプラグインが後から出力する要素に処理を加えたい場面では、「要素が出てくるまで待つ」関数が便利です。一定時間で諦める仕組みを入れておかないと、要素が出ないページで監視が続いてしまいます。
function waitForElement( selector, { root = document.documentElement, timeout = 10000 } = {} ) {
return new Promise( ( resolve, reject ) => {
const found = root.querySelector( selector );
if ( found ) {
resolve( found );
return;
}
const observer = new MutationObserver( () => {
const el = root.querySelector( selector );
if ( el ) {
observer.disconnect();
clearTimeout( timer );
resolve( el );
}
} );
const timer = setTimeout( () => {
observer.disconnect();
reject( new Error( 'timeout: ' + selector ) );
}, timeout );
observer.observe( root, { childList: true, subtree: true } );
} );
}
// 使用例:チャットウィジェットのボタンが出たらラベルを補う
waitForElement( '.example-chat-button' )
.then( ( button ) => {
if ( ! button.getAttribute( 'aria-label' ) ) {
button.setAttribute( 'aria-label', 'チャットで問い合わせる' );
}
} )
.catch( () => {} );
見つかった時点、またはタイムアウトの時点で disconnect() するので、監視が残り続けることはありません。ページ全体を見る document.documentElement は範囲が広いため、出現場所がわかっている場合は root に親要素を渡して範囲を狭めてください。
動作確認とデバッグのしかた
- 誰が DOM を変えているか調べる:Chrome の開発者ツールの「Elements」パネルで要素を右クリックし、「Break on」から「subtree modifications」などを選ぶと、その要素を変更した JavaScript の行で止まります(メニューの表記はバージョンによって異なることがあります)。監視すべき要素と、代わりに使えるイベントがないかを調べるのに役立ちます。
- コールバックの回数を数える:開発中はコールバックの中で回数を
console.count()し、スクロールやスライダーの動作中に想定外に増えていないか確かめます。 - 性能を確かめる:「Performance」パネルで記録し、MutationObserver のコールバックが長いタスクになっていないか確認します。
まとめ
- DOM を変える側にイベントやフック(CF7 の DOM イベント、WooCommerce の jQuery イベント、ブロックエディタの
wp.dataなど)があれば、そちらを優先します。 - 監視する要素は狭く、種類は
childListなど必要なものだけにします。body全体のattributes監視は避けます。 - コールバック内で DOM を変える場合は、処理済みの目印を付けるか、一時的に
disconnect()して無限ループを防ぎます。 - 追加されたノードは集めてから
requestAnimationFrameでまとめて処理し、要素を待つ処理にはタイムアウトを設けます。 - 開発者ツールの DOM 変更ブレークポイントで、変化の発生元を確認してから実装方針を決めます。