Реализация MutationObserver для реактивных обновлений DOM на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация MutationObserver для реактивных обновлений DOM на сайте
Средняя
~1 рабочий день
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация MutationObserver для реактивных обновлений DOM на сайте

MutationObserver следит за изменениями в DOM-дереве: добавление/удаление узлов, изменение атрибутов, изменение текстового содержимого. Работает асинхронно через микрозадачи — колбэк вызывается после завершения текущего синхронного кода, батчем мутаций.

Где это нужно: интеграция с легаси-кодом, сторонними виджетами, CMS-редакторами, аналитика изменений страницы, реализация custom elements без Web Components API, отслеживание динамически вставляемого контента.

Базовая настройка

const observer = new MutationObserver((mutations) => {
  for (const mutation of mutations) {
    switch (mutation.type) {
      case 'childList':
        // mutation.addedNodes — добавленные узлы (NodeList)
        // mutation.removedNodes — удалённые узлы
        break
      case 'attributes':
        // mutation.attributeName — имя атрибута
        // mutation.oldValue — старое значение (если attributeOldValue: true)
        break
      case 'characterData':
        // mutation.oldValue — старый текст (если characterDataOldValue: true)
        break
    }
  }
})

observer.observe(element, {
  childList: true,           // следить за добавлением/удалением дочерних узлов
  subtree: true,             // рекурсивно по всему поддереву
  attributes: true,          // следить за атрибутами
  attributeFilter: ['class', 'data-state'], // только эти атрибуты
  attributeOldValue: true,   // сохранять старое значение
  characterData: false,      // следить за текстовым содержимым
})

observer.disconnect() // отключить
observer.takeRecords() // получить накопленные мутации и очистить очередь

Ожидание появления элемента в DOM

Полезно для работы со сторонними виджетами, которые вставляют элементы асинхронно:

function waitForElement<T extends HTMLElement>(
  selector: string,
  root: HTMLElement | Document = document,
  timeoutMs = 10000
): Promise<T> {
  const existing = root.querySelector<T>(selector)
  if (existing) return Promise.resolve(existing)

  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      observer.disconnect()
      reject(new Error(`Элемент "${selector}" не появился за ${timeoutMs}ms`))
    }, timeoutMs)

    const observer = new MutationObserver(() => {
      const el = root.querySelector<T>(selector)
      if (el) {
        clearTimeout(timer)
        observer.disconnect()
        resolve(el)
      }
    })

    observer.observe(root, { childList: true, subtree: true })
  })
}

// Использование:
const chatWidget = await waitForElement<HTMLDivElement>('#intercom-container')
chatWidget.style.bottom = '80px' // переопределить стиль виджета

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

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

type ElementHandler = (element: HTMLElement) => (() => void) | void

function watchForElements(
  selector: string,
  handler: ElementHandler,
  root: HTMLElement | Document = document
): () => void {
  const cleanups = new Map<HTMLElement, () => void>()

  function processElement(el: HTMLElement): void {
    if (cleanups.has(el)) return
    const cleanup = handler(el)
    if (cleanup) cleanups.set(el, cleanup)
  }

  function processRemoval(el: HTMLElement): void {
    const cleanup = cleanups.get(el)
    if (cleanup) {
      cleanup()
      cleanups.delete(el)
    }
  }

  // Инициализируем существующие элементы
  root.querySelectorAll<HTMLElement>(selector).forEach(processElement)

  const observer = new MutationObserver((mutations) => {
    for (const mutation of mutations) {
      mutation.addedNodes.forEach((node) => {
        if (node.nodeType !== Node.ELEMENT_NODE) return
        const el = node as HTMLElement
        if (el.matches(selector)) processElement(el)
        el.querySelectorAll<HTMLElement>(selector).forEach(processElement)
      })

      mutation.removedNodes.forEach((node) => {
        if (node.nodeType !== Node.ELEMENT_NODE) return
        const el = node as HTMLElement
        if (el.matches(selector)) processRemoval(el)
        el.querySelectorAll<HTMLElement>(selector).forEach(processRemoval)
      })
    }
  })

  observer.observe(root, { childList: true, subtree: true })

  return () => {
    observer.disconnect()
    cleanups.forEach((cleanup) => cleanup())
    cleanups.clear()
  }
}

// Пример: автоматически инициализировать кастомные компоненты
const stop = watchForElements('[data-tooltip]', (el) => {
  const tooltip = new TooltipController(el)
  return () => tooltip.destroy()
})

Отслеживание изменений атрибутов

function watchAttribute(
  element: HTMLElement,
  attribute: string,
  onChange: (newValue: string | null, oldValue: string | null) => void
): () => void {
  const observer = new MutationObserver((mutations) => {
    for (const mutation of mutations) {
      if (mutation.attributeName === attribute) {
        onChange(
          element.getAttribute(attribute),
          mutation.oldValue
        )
      }
    }
  })

  observer.observe(element, {
    attributes: true,
    attributeFilter: [attribute],
    attributeOldValue: true,
  })

  return () => observer.disconnect()
}

// Синхронизация с классом стороннего компонента
watchAttribute(someWidget, 'class', (newValue, oldValue) => {
  const wasOpen = oldValue?.includes('is-open')
  const isOpen = newValue?.includes('is-open')
  if (!wasOpen && isOpen) onWidgetOpen()
  if (wasOpen && !isOpen) onWidgetClose()
})

React-хук

function useMutationObserver(
  target: HTMLElement | null,
  callback: MutationCallback,
  options: MutationObserverInit
): void {
  const callbackRef = useRef(callback)
  callbackRef.current = callback

  useEffect(() => {
    if (!target) return

    const observer = new MutationObserver((...args) => callbackRef.current(...args))
    observer.observe(target, options)
    return () => observer.disconnect()
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [target, JSON.stringify(options)])
}

// Использование:
function DynamicContent() {
  const containerRef = useRef<HTMLDivElement>(null)
  const [childCount, setChildCount] = useState(0)

  useMutationObserver(
    containerRef.current,
    (mutations) => {
      setChildCount(containerRef.current?.childElementCount ?? 0)
    },
    { childList: true }
  )

  return <div ref={containerRef}>{/* динамическое содержимое */}</div>
}

Производительность

MutationObserver может накапливать тысячи мутаций в секунду при активных DOM-изменениях. Несколько правил:

  • Не использовать subtree: true без необходимости — дорогостоящее наблюдение
  • Фильтровать мутации внутри колбэка максимально быстро
  • Использовать observer.takeRecords() для принудительного сброса очереди перед disconnect
  • Не обращаться к DOM внутри колбэка без необходимости — каждый querySelector это layout query

Срок: 0.5–1 день в зависимости от сложности сценариев.