SPA や複雑な Web アプリにコンテンツスクリプトを注入する場合、起動直後はどの要素が DOM に存在するかわからない。最初から細粒度のオブザーバーを設定しようとしても、対象要素がまだ存在しないため空振りする。
段階的 DOM 観測戦略はこの問題を 2 フェーズで解決する。
- 待機フェーズ:アプリルート全体を広く監視し、「準備完了シグナル」となる要素の出現を待つ
- アクティブフェーズ:シグナルが検出されたら、各領域にスコープを絞ったオブザーバーへ切り替える。URL 変化を検知したら待機フェーズへ戻る
コード
type ObserverLayer = { selector: string callback: MutationCallback options?: MutationObserverInit}
const activeObservers = new Map<string, MutationObserver>()
const detachAll = (): void => { for (const obs of activeObservers.values()) { obs.disconnect() } activeObservers.clear()}
const attachLayer = (layer: ObserverLayer): void => { const el = document.querySelector(layer.selector) if (!el) return activeObservers.get(layer.selector)?.disconnect() const obs = new MutationObserver(layer.callback) obs.observe(el, layer.options ?? { childList: true, subtree: true }) activeObservers.set(layer.selector, obs)}
export const createLayeredObserver = ( appRootSelector: string, readySelector: string, layers: ObserverLayer[],): (() => void) => { let lastUrl = location.href let phase: 'waiting' | 'active' = 'waiting'
const rootObserver = new MutationObserver(() => { const currentUrl = location.href
if (currentUrl !== lastUrl) { lastUrl = currentUrl phase = 'waiting' detachAll() }
if (phase === 'waiting' && document.querySelector(readySelector)) { phase = 'active' detachAll() for (const layer of layers) { attachLayer(layer) } } })
const root = document.querySelector(appRootSelector) ?? document.body rootObserver.observe(root, { childList: true, subtree: true })
return () => { rootObserver.disconnect() detachAll() }}使用例
const teardown = createLayeredObserver( '#app', // アプリルート(広域監視) '[data-ready]', // 準備完了シグナル要素 [ { selector: '.stream', callback: (mutations) => { for (const m of mutations) console.log('stream:', m.type) }, }, { selector: '.sidebar', callback: (mutations) => { for (const m of mutations) console.log('sidebar:', m.type) }, }, { selector: '[role="dialog"]', callback: (mutations) => { for (const m of mutations) console.log('modal:', m.type) }, }, ],)
// すべてのオブザーバーを停止teardown()仕組み
rootObserverはアプリルートを{ childList: true, subtree: true }で広く監視する- コールバックが呼ばれるたびに
location.hrefを前回値と比較し、URL が変わっていればルート変更とみなして全レイヤーオブザーバーを破棄し、phaseを'waiting'へ戻す phase === 'waiting'かつreadySelectorが DOM に存在すればphaseを'active'にし、各レイヤーにMutationObserverを付け直す- ルートオブザーバーは常に稼働し続けるため、SPA ナビゲーションにも追従できる
応用
このパターンは特定のフレームワークやセレクターセットに依存しない。以下のユースケースに応用できる。
- SPA コンテンツスクリプト:Chrome Extension や Userscript でルート変更に連動して処理を再初期化する
- サードパーティ DOM 統合:外部サービスが埋め込まれたページに後乗りでオブザーバーを設定する
- アナリティクスオーバーレイ:特定領域の変化だけを監視してイベントを送信する
- オートエンハンサー:複雑な Web アプリに注入して要素の出現・消滅を追う
hsb.horse