Реалізація 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-редакторами, аналітика змін сторінки, реалізація користувацьких елементів без 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 Hook

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 день залежно від складності сценаріїв.