作成日:2025/8/7,最終更新日:2025/8/13

JavaScript / TypeScriptのDOM操作とタイミングの深い関係:応用編(監視と制御のテクニック)

IntersectionObserver / MutationObserver / ResizeObserver / イベントループ など、DOMをどう「見張り」「制御する」かを徹底解説します。

この応用編では、DOMの変化・可視範囲・サイズなどに反応して処理を実行するための技術を紹介します。
また、setTimeout や Promise などがいつ実行されるのかといったタイミング制御の仕組み(イベントループ)についても掘り下げます。


1. IntersectionObserverの活用

解説: IntersectionObserver は、特定の要素が画面内に現れたかどうかを検知できる便利なAPIです。
画像の遅延読み込みやアニメーションの発火タイミング制御など、スクロールに応じた処理に広く活用されています。




● 基本構文と使い方

まずは IntersectionObserver の基本的な使い方を見てみましょう。

const target = document.getElementById('box');

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      console.log('要素が画面内に入りました');
    }
  });
});

observer.observe(target);

このように、observe() で監視対象を指定すると、要素が画面内に入った瞬間にコールバックが実行されます。


● オプション指定で動作を調整

第二引数で thresholdrootMargin などのオプションを設定できます。

const observer = new IntersectionObserver(callback, {
  threshold: 0.5,         // 要素が50%以上見えたら発火
  rootMargin: '0px 0px -20% 0px' // 少し手前でトリガーさせる
});

スクロールの余裕や発火の閾値を調整したいときに便利です。


● 活用例:画像の遅延読み込み(lazy-load)

最もよく使われるのが、画像の遅延読み込みです。data-src で画像パスを一時的に持たせておき、表示直前に読み込みます。

const imgs = document.querySelectorAll('img[data-src]');

const observer = new IntersectionObserver((entries, obs) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      obs.unobserve(img); // 一度読み込んだら監視解除
    }
  });
});

imgs.forEach(img => observer.observe(img));

表示されていない画像の読み込みを遅らせることで、ページの初期表示を高速化できます。


● まとめ

  • IntersectionObserver は要素の可視状態を効率よく監視できる
  • ✅ アニメーション発火・広告表示・遅延読み込みなどに活用される
  • ✅ 従来の scroll イベントよりもパフォーマンスに優れる

次は、DOMの「変化」そのものを監視できる MutationObserver について解説します。


● 実行例:要素が見えたら背景とテキストを変える

下にスクロールして、灰色のブロック(#box)が画面内に入ると、背景が濃い緑になり、テキストも変わります。

監視中の要素(まだ見えていません)

2. MutationObserverでDOMの変化を監視

解説: MutationObserver は、DOMツリーに対する変更(ノードの追加・削除、属性の変更など)を検知するためのAPIです。
旧来の Mutation Events よりも高速・安定しており、SPAや動的UIでの監視処理に多用されます。




● 基本構文と使い方

監視対象としたい要素を指定し、その中で発生したDOMの変更を検出します。

const target = document.getElementById('target');

const observer = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    console.log('DOMに変更がありました:', mutation);
  });
});

observer.observe(target, {
  childList: true,         // 子ノードの追加・削除を監視
  attributes: true,        // 属性の変更を監視
  subtree: true            // 子孫ノードも含めて監視
});

監視内容は observe() の第2引数で細かく指定できます。


● 活用例:動的に追加された要素を検出

ユーザー操作や非同期処理で追加された要素に対して、後から処理を加えるようなケースに活用できます。

const logArea = document.getElementById('log');
const observer = new MutationObserver(() => {
  logArea.textContent = '✅ 新しい要素が追加されました!';
});

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

innerHTML の操作などで中身が書き換えられたタイミングを検出できます。


● 実行例:ボタンで要素を追加し、検出する

下のボタンを押すと、ログエリアに新しい段落が追加され、それをMutationObserverが検出します。


● まとめ

  • MutationObserver はDOMの追加・削除・属性変更などを検出できる
  • ✅ SPAや非同期で構成が変わるページに最適
  • observe() のオプション指定で柔軟な監視が可能

次は、要素のサイズ変化を検出できる ResizeObserver を見ていきましょう。


3. ResizeObserverでサイズ変化を監視

解説: ResizeObserver は、要素の表示サイズ(コンテンツボックス)の変化をリアルタイムに検知できるAPIです。
画面幅に応じたレイアウト切り替え、エディタやグラフの自動リサイズ、文字の折返しに伴う高さ変化への追従などに役立ちます。




● 基本構文と使い方

監視したい要素を observe で登録すると、サイズが変化したタイミングでコールバックが呼ばれます。

const box = document.getElementById('target');

const ro = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const cr = entry.contentRect; // 直近のコンテンツ領域のサイズ
    console.log(`width: ${cr.width}, height: ${cr.height}`);
  }
});

ro.observe(box);

● 何がトリガーになる?(よくある変化例)

  • スタイル変更(例:width / height / padding など)
  • テキストの折返しや画像読み込みで要素の高さが変わる
  • レスポンシブ対応で親幅に合わせて子要素が縮む・伸びる

● 活用例1:テキストエリアの自動リサイズに連動して表示を更新

入力量に応じて高さが変わる要素を監視し、サイズに合わせて別UIを更新できます。

const textarea = document.getElementById('memo');
const sizeLabel = document.getElementById('sizeLabel');

const ro = new ResizeObserver((entries) => {
  const { width, height } = entries[0].contentRect;
  sizeLabel.textContent = `サイズ: ${Math.round(width)} x ${Math.round(height)}`;
});

ro.observe(textarea);

textarea.addEventListener('input', () => {
  textarea.style.height = 'auto';
  textarea.style.height = textarea.scrollHeight + 'px';
});

● 実行例:ボタンでサイズを変えて変化を検知

ボタンを押すとボックスの幅が増減し、ResizeObserver が変化を検知してサイズ表示を更新します。

サイズ: -
監視対象

● 注意点とベストプラクティス

  • 頻繁に発火するため、コールバック内で重い処理は避ける(requestAnimationFrame や setTimeout で間引くのも有効)
  • 不要になったら unobserve または disconnect で監視解除し、メモリリークを防ぐ
  • 監視対象は「コンテンツ領域」なので、枠線やマージンなど外側のサイズは直接の対象外

● まとめ

  • 要素のサイズ変化をリアルタイムに検知でき、レイアウト連動UIに最適
  • テキスト折返しやレスポンシブ変化にも強い
  • 高頻度発火を想定し、処理の最適化と監視解除を心がける

次は、非同期処理の順序を理解するためにイベントループとマイクロタスクの順序を解説します。


4. イベントループとマイクロタスクの順序

解説: JavaScriptはシングルスレッドで動作し、処理の順序はイベントループが管理します。
非同期処理は大きくマクロタスク(例:setTimeout / setInterval)とマイクロタスク(例:Promise.then / queueMicrotask / MutationObserver)に分かれ、同一フレーム内ではマイクロタスクが先に処理されます。




● 基本の流れ

1) 同期処理(現在のタスク) → 2) マイクロタスクをすべて実行 → 3) 次の描画前コールバック(requestAnimationFrame など)→ 4) 次のマクロタスク(例:setTimeout)…という順に進みます。

console.log('同期処理1');

setTimeout(() => {
  console.log('setTimeout(マクロタスク)');
}, 0);

Promise.resolve().then(() => {
  console.log('Promise.then(マイクロタスク)');
});

queueMicrotask(() => {
  console.log('queueMicrotask(マイクロタスク)');
});

console.log('同期処理2');

// 期待される出力順:
// 同期処理1
// 同期処理2
// Promise.then(マイクロタスク)   ← 今回は、先に登録しているためこちらが先
// queueMicrotask(マイクロタスク)
// (※ rAF を追加していない例なのでここでは出ません)
// setTimeout(マクロタスク)

● マクロタスクとマイクロタスクの違い

種類代表例実行タイミング
マクロタスク setTimeout, setInterval, メインのI/Oコールバック など イベントループの「次のラウンド」で実行
マイクロタスク Promise.then / catch / finally, queueMicrotask, MutationObserver 現在のマクロタスクが終わった直後に全て実行

● 実行例:順序の違いを体験

以下のスクリプトは、同期処理・マイクロタスク・マクロタスク・描画タイミング(requestAnimationFrame)の順序をコンソールに記録します。
ブラウザの開発者ツールでconsoleを開いて確認してください。
※この実行例は、上記の「基本の流れ」に加えて「requestAnimationFrame」と「MutationObserver」を追加し、描画タイミングやDOM変化検知も含めた順序を確認できるようにしています。
※スマホやタブレットでconsoleを開いて確認するには、この記事参考にしてね! ≫ここから記事へ≪


● よくあるハマりどころ

  • Promise.then や queueMicrotask は setTimeout より先に実行される(同フレーム内)
  • 大量のマイクロタスクをキューに詰めると、描画や入力応答が遅れることがある
  • 描画タイミングに合わせたい時は requestAnimationFrame を使う(セクション4参照)

● 使い分けの指針

  • すぐに後続処理を走らせたい軽量な後処理 → マイクロタスク(Promise.then / queueMicrotask)
  • フレームをまたいで遅らせたい、UIをブロックしたくない処理 → マクロタスク(setTimeout)
  • レイアウト・描画に同期したい処理 → requestAnimationFrame

● まとめ

  • イベントループは「同期 → マイクロタスク全実行 → 次のマクロタスク → 必要なら描画」の順で進む
  • マイクロタスクは同一ラウンド内ですべて消化されるため、setTimeoutより先に走る
  • 用途に応じて、マクロタスク・マイクロタスク・描画コールバックを使い分ける