MutationObserver: наблюдение за DOM

MutationObserver — браузерный API для асинхронного наблюдения за изменениями в DOM-дереве: добавлением/удалением узлов, изменением атрибутов и текстового содержимого.

Зачем нужно

MutationObserver — замена устаревшим Mutation Events, работающий асинхронно (батчинг) и не блокирующий рендеринг. Используется когда нужно реагировать на изменения DOM, сделанные сторонним кодом (библиотеками, расширениями), или следить за динамически добавляемым контентом.

Где используется

  • Инструменты разработчика и расширения браузера
  • Отслеживание изменений в rich text редакторах
  • Lazy loading: реагировать на появление элементов в DOM
  • Интеграция с legacy-кодом, который напрямую модифицирует DOM
  • Тесты: ожидать появление определённого элемента

Основной контент

Базовое использование

// Создаём наблюдатель
const observer = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    console.log('Тип мутации:', mutation.type);

    if (mutation.type === 'childList') {
      mutation.addedNodes.forEach(node => {
        console.log('Добавлен:', node);
      });
      mutation.removedNodes.forEach(node => {
        console.log('Удалён:', node);
      });
    }

    if (mutation.type === 'attributes') {
      console.log(`Атрибут "${mutation.attributeName}" изменён`);
      console.log('Старое значение:', mutation.oldValue);
    }
  });
});

// Подключаем к элементу
const target = document.getElementById('container');

observer.observe(target, {
  childList: true,       // добавление/удаление дочерних узлов
  attributes: true,      // изменение атрибутов
  subtree: true,         // наблюдать за всем поддеревом
  attributeOldValue: true, // сохранять старое значение атрибута
  characterData: true,   // изменение текста
});

// Остановить наблюдение
observer.disconnect();

Наблюдение за добавлением элементов

// Ждём появления элемента в DOM (lazy init)
function waitForElement(selector) {
  return new Promise(resolve => {
    const existing = document.querySelector(selector);
    if (existing) return resolve(existing);

    const observer = new MutationObserver(() => {
      const el = document.querySelector(selector);
      if (el) {
        observer.disconnect();
        resolve(el);
      }
    });

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

waitForElement('#dynamic-widget').then(el => {
  console.log('Элемент появился:', el);
  el.addEventListener('click', handleClick);
});

Батчинг и производительность

// MutationObserver батчит изменения — callback вызывается один раз
// даже при множественных синхронных изменениях
const list = document.getElementById('list');
const observer = new MutationObserver(mutations => {
  console.log(`Получено ${mutations.length} мутаций`);
});

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

// Три добавления — одна запись в callback (батч)
for (let i = 0; i < 3; i++) {
  const li = document.createElement('li');
  li.textContent = `Пункт ${i}`;
  list.appendChild(li);
}
// Один вызов callback с массивом из 3 мутаций

Частые ошибки

  • Забыть вызвать disconnect — наблюдатель держит ссылку на target, мешая сборщику мусора. Всегда отключайте в cleanup (при удалении компонента).
  • Наблюдение за document.body с subtree: true — очень дорого по производительности. Наблюдайте за минимально необходимым поддеревом.
  • Бесконечный цикл — если callback сам изменяет DOM, это вызывает новые мутации. Используйте флаг или проверяйте, что изменение сделано самим наблюдателем.

Связанные темы

Ресурсы