MutationObserver — наблюдение за изменениями DOM

17 июля 2026
Автор

Антон Ларичев

Что такое MutationObserver

MutationObserver — это встроенный браузерный API, позволяющий асинхронно отслеживать изменения в дереве DOM: добавление и удаление узлов, изменение атрибутов, модификацию текстового содержимого. Он пришёл на смену устаревшим MutationEvents (DOMNodeInserted, DOMAttrModified и другим), которые работали синхронно и сильно нагружали браузер.

Механизм работает следующим образом: наблюдатель ставится в очередь микрозадач и срабатывает после завершения текущей задачи, но до следующей макрозадачи. Это делает его эффективным инструментом без риска бесконечных циклов при изменении DOM внутри коллбека.

Создание наблюдателя

Конструктор принимает один аргумент — функцию обратного вызова, которая будет вызвана при каждой порции изменений:

const observer = new MutationObserver((mutations, observerRef) => {
  mutations.forEach((mutation) => {
    console.log(mutation);
  });
});

Коллбек получает два параметра:

  • mutations — массив объектов MutationRecord, каждый описывает одно изменение;
  • observerRef — ссылка на сам экземпляр MutationObserver, удобна для отключения изнутри коллбека.

Запуск наблюдения: метод observe

Чтобы начать наблюдение, нужно вызвать observe и передать целевой узел и объект с настройками:

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

observer.observe(target, {
  childList: true,
  subtree: true,
  attributes: true,
  attributeFilter: ['class', 'data-state'],
  characterData: true,
});

Параметры конфигурации

Параметр Тип Описание
childList boolean Отслеживать добавление и удаление дочерних узлов
subtree boolean Распространить наблюдение на всё поддерево
attributes boolean Отслеживать изменения атрибутов
attributeFilter string[] Список атрибутов для наблюдения (если не указан — все атрибуты)
attributeOldValue boolean Сохранять старое значение атрибута в MutationRecord
characterData boolean Отслеживать изменения текстовых узлов
characterDataOldValue boolean Сохранять старое текстовое содержимое

Важно: хотя бы один из параметров childList, attributes или characterData должен быть true, иначе браузер выбросит ошибку.

Объект MutationRecord

Каждая запись в массиве mutations является экземпляром MutationRecord и содержит следующие свойства:

const observer = new MutationObserver((mutations) => {
  mutations.forEach((record) => {
    console.log('Тип изменения:', record.type);           // 'childList' | 'attributes' | 'characterData'
    console.log('Целевой узел:', record.target);          // узел, на котором произошло изменение
    console.log('Добавленные узлы:', record.addedNodes);  // NodeList добавленных узлов
    console.log('Удалённые узлы:', record.removedNodes);  // NodeList удалённых узлов
    console.log('Имя атрибута:', record.attributeName);   // имя изменённого атрибута или null
    console.log('Старое значение:', record.oldValue);     // старое значение (если запрошено)
    console.log('Предыдущий сосед:', record.previousSibling); // предшествующий узел
    console.log('Следующий сосед:', record.nextSibling);      // следующий узел
  });
});

Практические примеры

Отслеживание добавления элементов

Часто используется для реакции на динамически вставляемый контент — например, из стороннего виджета или lazy-loaded модуля:

function waitForElement(selector) {
  return new Promise((resolve) => {
    const existing = document.querySelector(selector);
    if (existing) {
      resolve(existing);
      return;
    }

    const observer = new MutationObserver((mutations, obs) => {
      const element = document.querySelector(selector);
      if (element) {
        obs.disconnect();
        resolve(element);
      }
    });

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

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

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

Полезно для синхронизации состояния компонента с его атрибутами, особенно при работе с Web Components:

const button = document.querySelector('#submit-btn');

const observer = new MutationObserver((mutations) => {
  mutations.forEach((record) => {
    if (record.type === 'attributes' && record.attributeName === 'disabled') {
      const isDisabled = button.hasAttribute('disabled');
      console.log(`Кнопка ${isDisabled ? 'заблокирована' : 'активна'}`);
      updateButtonStyles(button, isDisabled);
    }
  });
});

observer.observe(button, {
  attributes: true,
  attributeFilter: ['disabled'],
  attributeOldValue: true,
});

Отслеживание изменений текста

const liveRegion = document.querySelector('[aria-live]');

const observer = new MutationObserver((mutations) => {
  mutations.forEach((record) => {
    if (record.type === 'characterData') {
      console.log('Старый текст:', record.oldValue);
      console.log('Новый текст:', record.target.textContent);
    }
  });
});

observer.observe(liveRegion, {
  characterData: true,
  subtree: true,
  characterDataOldValue: true,
});

Автоматическое добавление класса к новым элементам

const container = document.querySelector('.card-grid');

const observer = new MutationObserver((mutations) => {
  mutations.forEach((record) => {
    record.addedNodes.forEach((node) => {
      if (node.nodeType === Node.ELEMENT_NODE && node.matches('.card')) {
        node.classList.add('card--animated');
        node.addEventListener('animationend', () => {
          node.classList.remove('card--animated');
        }, { once: true });
      }
    });
  });
});

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

Остановка наблюдения: метод disconnect

Метод disconnect останавливает наблюдение и освобождает ресурсы. Его необходимо вызывать, когда наблюдатель больше не нужен, чтобы избежать утечек памяти:

const observer = new MutationObserver(handleMutations);
observer.observe(target, { childList: true, subtree: true });

// Остановить наблюдение через 10 секунд
setTimeout(() => {
  observer.disconnect();
  console.log('Наблюдение остановлено');
}, 10000);

Внутри коллбека можно использовать второй параметр — ссылку на сам наблюдатель:

const observer = new MutationObserver((mutations, obs) => {
  const found = [...mutations].some((m) =>
    [...m.addedNodes].some((n) => n.matches?.('.target'))
  );

  if (found) {
    obs.disconnect(); // наблюдатель отключает сам себя
    doSomething();
  }
});

Метод takeRecords

takeRecords позволяет получить накопленные, но ещё не переданные в коллбек записи и очистить внутреннюю очередь. Используется перед вызовом disconnect, если нужно обработать все оставшиеся изменения:

const pending = observer.takeRecords();
if (pending.length > 0) {
  handleMutations(pending);
}
observer.disconnect();

Наблюдение за несколькими узлами

Один экземпляр MutationObserver может наблюдать за несколькими элементами. Каждый вызов observe добавляет новую цель:

const observer = new MutationObserver((mutations) => {
  mutations.forEach((record) => {
    console.log('Изменение в:', record.target.id);
  });
});

const config = { attributes: true, childList: true };

document.querySelectorAll('.widget').forEach((el) => {
  observer.observe(el, config);
});

// Один вызов disconnect остановит наблюдение за всеми целями
observer.disconnect();

Типичные ловушки

Бесконечный цикл

Если коллбек сам изменяет DOM, это может породить бесконечный цикл. Решения: использовать флаг, фильтровать тип изменений или временно отключать наблюдатель:

let ignoreNextMutation = false;

const observer = new MutationObserver((mutations) => {
  if (ignoreNextMutation) {
    ignoreNextMutation = false;
    return;
  }

  mutations.forEach((record) => {
    ignoreNextMutation = true;
    record.target.setAttribute('data-updated', Date.now());
  });
});

observer.observe(target, { attributes: true, attributeFilter: ['class'] });

Утечка памяти

Если наблюдаемый элемент удаляется из DOM, а наблюдатель не отключён, браузер продолжит удерживать ссылки. Всегда вызывайте disconnect при размонтировании компонента:

class Widget {
  constructor(el) {
    this.el = el;
    this.observer = new MutationObserver(this.handleMutations.bind(this));
    this.observer.observe(el, { childList: true, subtree: true });
  }

  handleMutations(mutations) {
    // обработка
  }

  destroy() {
    this.observer.disconnect();
    this.el = null;
  }
}

Задержка коллбека

MutationObserver работает асинхронно. Изменения DOM не будут видны в коллбеке немедленно после их применения — они придут в следующей микрозадаче. Если нужно синхронное поведение, MutationObserver не подходит.

Сравнение с устаревшими MutationEvents

Критерий MutationEvents MutationObserver
Синхронность Синхронные Асинхронные (микрозадачи)
Производительность Низкая (каждое событие по отдельности) Высокая (пакетная обработка)
Риск рекурсии Высокий Минимальный
Поддержка браузеров Устарело, удалено из спецификации Все современные браузеры
API addEventListener observe / disconnect

MutationEvents официально удалены из веб-стандартов и не должны использоваться в новом коде.

Интеграция с современными фреймворками

В React MutationObserver удобно использовать внутри useEffect с правильной очисткой:

import { useEffect, useRef } from 'react';

function useAttributeWatcher(ref, attributeName, callback) {
  useEffect(() => {
    const el = ref.current;
    if (!el) return;

    const observer = new MutationObserver((mutations) => {
      mutations.forEach((record) => {
        if (record.attributeName === attributeName) {
          callback(el.getAttribute(attributeName), record.oldValue);
        }
      });
    });

    observer.observe(el, {
      attributes: true,
      attributeFilter: [attributeName],
      attributeOldValue: true,
    });

    return () => observer.disconnect();
  }, [ref, attributeName, callback]);
}

// Использование
function MyComponent() {
  const divRef = useRef(null);

  useAttributeWatcher(divRef, 'aria-expanded', (newVal, oldVal) => {
    console.log(`aria-expanded: ${oldVal} -> ${newVal}`);
  });

  return <div ref={divRef} aria-expanded="false">Контент</div>;
}

Поддержка браузеров

MutationObserver поддерживается во всех современных браузерах: Chrome 26+, Firefox 14+, Safari 7+, Edge 12+. В Node.js API недоступен нативно, но существуют полифиллы для серверного тестирования (например, mutationobserver-shim).

Хорошие практики

  • Всегда вызывайте disconnect при уничтожении компонента или завершении работы наблюдателя.
  • Используйте attributeFilter, чтобы ограничить список отслеживаемых атрибутов — это снижает нагрузку.
  • Не устанавливайте subtree: true без необходимости на крупных поддеревьях.
  • Пакетируйте обработку изменений: массив mutations может содержать десятки записей за один вызов коллбека.
  • Избегайте тяжёлых вычислений прямо в коллбеке — лучше использовать requestAnimationFrame или requestIdleCallback для откладывания работы.

Подробнее о работе с DOM, событиями и браузерными API вы найдёте на курсе по JavaScript: https://purpleschool.ru/course/javascript?utm_source=knowledgebase&utm_medium=text&utm_campaign=mutationobserver

Стрелочка влевоJavaScript ResizeObserverIntersectionObserver в JavaScriptСтрелочка вправо

Постройте личный план изучения Javascript до уровня Middle — бесплатно!

Javascript — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по Javascript

Как работает метод trim() - JavaScriptКак работает метод toUpperCase() - JavaScriptКак работает метод toLowerCase() - JavaScriptКак работает метод substring() - JavaScriptКак работает метод startsWith() - JavaScriptКак работает метод split() - JavaScriptКак работает метод slice() - JavaScriptКак работает метод search() - JavaScriptКак работает метод replaceAll() - JavaScriptКак работает метод replace() - JavaScriptКак работает метод repeat() - JavaScriptКак работает метод padStart() - JavaScriptКак работает метод padEnd() - JavaScriptКак работает метод matchAll() - JavaScriptКак работает метод match() - JavaScriptКак работает метод localeCompare() - JavaScriptКак работает свойство length - JavaScriptКак работает метод lastIndexOf() - JavaScriptКак работает метод indexOf() - JavaScriptКак работает метод includes() - JavaScriptКак работает метод fromCodePoint() - JavaScriptКак работает метод fromCharCode() - JavaScriptКак работает метод endsWith() - JavaScriptКак работает метод concat() - JavaScriptКак работает метод codePointAt() - JavaScriptКак работает метод charCodeAt() - JavaScriptКак работает метод charAt() - JavaScript
Итератор в JavaScript
try...catch в JavaScriptError в JavaScript
Событие wheel в JavaScriptСобытие unload в JavaScriptСобытие touch в JavaScriptСобытие submit в JavaScriptСобытие scroll в JavaScriptСобытие reset в JavaScriptМетод .preventDefault() в JavaScriptСобытие mouseover в JavaScriptСобытие mouseout в JavaScriptСобытие load в JavaScriptСобытие keyup в JavaScriptСобытие keydown в JavaScriptСобытие invalid в JavaScriptСобытие input в JavaScriptСобытийная модель Event в JavaScriptОбъект события Event в JavaScriptСобытие DOMContentLoaded в JavaScriptСобытие dblclick в JavaScriptСобытие click в JavaScriptСобытие change в JavaScriptСобытие beforeunload в JavaScript
Как работает метод some() - JavaScriptКак работает метод reverse() - JavaScriptКак работает метод reduce() - JavaScriptКак работает метод map() - JavaScriptКак работает метод isArray() - JavaScriptКак работает метод indexOf() - JavaScriptКак работает метод includes() - JavaScriptКак работает метод from() - JavaScriptКак работает метод forEach() - JavaScriptКак работает метод flatMap() - JavaScriptКак работает метод flat() - JavaScriptКак работает метод findIndex() - JavaScriptКак работает метод find() - JavaScriptКак работает метод filter() - JavaScriptКак работает метод every() - JavaScriptМассивы в JavaScriptArray.at, findLast, findLastIndex — новые методы массивов
Открыть базу знаний

Лучшие курсы по теме

изображение курса

Основы JavaScript

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий