MutationObserver

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

Библиотека Tabbable предназначена для определения элементов, которые могут получать фокус при последовательной навигации. Однако если структура документа изменяется после первоначального вычисления, полученный список элементов может устареть. В таких ситуациях используется механизм наблюдения за изменениями DOM — MutationObserver.

MutationObserver представляет собой встроенный API браузера, позволяющий отслеживать изменения в DOM-дереве. Он реагирует на:

  • добавление и удаление узлов;
  • изменение атрибутов элементов;
  • изменения текстового содержимого;
  • перестроение вложенной структуры элементов.

Использование MutationObserver позволяет автоматически обнаруживать изменения, влияющие на доступность элементов для фокуса, и своевременно пересчитывать список tabbable-элементов.


Проблема динамических интерфейсов

Современные интерфейсы активно используют:

  • динамическую подгрузку компонентов;
  • модальные окна;
  • раскрывающиеся меню;
  • виртуализированные списки;
  • UI-фреймворки (React, Vue, Svelte).

В результате структура DOM изменяется значительно чаще, чем в классических статических страницах.

Пример ситуации:

  1. На странице существует список элементов, среди которых имеются кнопки и ссылки.
  2. При нажатии на кнопку открывается модальное окно.
  3. Внутри модального окна появляется новый набор интерактивных элементов.
  4. Для корректной доступности клавиатурная навигация должна переключиться на новый контейнер.

Если список tabbable-элементов вычислялся ранее, он не будет содержать элементы модального окна. Это нарушит ожидаемую последовательность фокуса.

MutationObserver позволяет обнаружить появление нового DOM-фрагмента и инициировать повторный анализ.


Основы API MutationObserver

MutationObserver создаётся через конструктор:

const observer = new MutationObserver(callback);

Аргумент callback — функция, вызываемая при обнаружении изменений.

function callback(mutationsList, observer) {
  for (const mutation of mutationsList) {
    console.log(mutation.type);
  }
}

Каждый объект mutation описывает конкретное изменение.

Основные типы мутаций:

  • childList — добавление или удаление дочерних узлов
  • attributes — изменение атрибутов
  • characterData — изменение текстового содержимого

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

observer.observe(targetNode, options);

Пример настроек:

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

Значение subtree: true означает отслеживание изменений во всех вложенных элементах.


Типы изменений, влияющих на tabbable-элементы

Для корректной работы навигации по фокусу важны следующие изменения DOM.

Добавление элементов

Появление новых элементов может изменить порядок табуляции.

Примеры:

  • добавление кнопки
  • появление ссылки
  • открытие формы
  • создание модального окна

MutationObserver позволяет обнаружить такие элементы сразу после их вставки в DOM.


Удаление элементов

Удаление интерактивных элементов может нарушить текущую позицию фокуса.

Например:

  • удаление кнопки, на которой находился фокус
  • закрытие модального окна
  • удаление панели управления

Observer фиксирует удаление узлов через тип мутации childList.


Изменение атрибутов

Некоторые атрибуты напрямую влияют на возможность получения фокуса:

  • tabindex
  • disabled
  • hidden
  • aria-hidden
  • style (например display: none)

Пример изменения:

button.setAttribute("disabled", true);

После этого элемент перестаёт быть tabbable. MutationObserver позволяет обнаружить изменение атрибутов и обновить список элементов.


Использование MutationObserver для обновления tabbable-списка

Типичная схема работы выглядит следующим образом:

  1. Определяется контейнер интерфейса.
  2. Выполняется вычисление tabbable-элементов.
  3. Запускается наблюдение за изменениями DOM.
  4. При изменениях список пересчитывается.

Пример:

import { tabbable } from 'tabbable';

const container = document.querySelector('#app');

function updateTabbables() {
  const elements = tabbable(container);
  console.log(elements);
}

const observer = new MutationObserver(() => {
  updateTabbables();
});

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

updateTabbables();

Каждое изменение DOM приводит к обновлению списка доступных элементов.


Фильтрация мутаций

MutationObserver может генерировать большое количество событий. Не каждое изменение требует пересчёта tabbable-элементов.

Эффективная реализация анализирует тип мутации.

Пример фильтрации:

const observer = new MutationObserver((mutations) => {
  let shouldUpdate = false;

  for (const mutation of mutations) {
    if (mutation.type === "childList") {
      shouldUpdate = true;
      break;
    }

    if (mutation.type === "attributes") {
      if (mutation.attributeName === "tabindex") {
        shouldUpdate = true;
        break;
      }
    }
  }

  if (shouldUpdate) {
    updateTabbables();
  }
});

Такой подход снижает нагрузку на производительность.


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

MutationObserver позволяет ограничить список отслеживаемых атрибутов.

observer.observe(container, {
  attributes: true,
  attributeFilter: ["tabindex", "disabled", "hidden"],
  subtree: true
});

Это уменьшает количество ненужных уведомлений.


Оптимизация производительности

При активном изменении DOM пересчёт tabbable-элементов может выполняться слишком часто. Для предотвращения этого применяются методы оптимизации.

Debounce

Обновление откладывается на короткий промежуток времени.

let timer;

const observer = new MutationObserver(() => {
  clearTimeout(timer);

  timer = setTimeout(() => {
    updateTabbables();
  }, 50);
});

Несколько изменений объединяются в одно обновление.


Проверка реального влияния на фокус

Иногда добавленный элемент не влияет на фокусируемость.

Например:

<div class="decorative"></div>

Такие элементы можно игнорировать.

Проверка может выполняться по тегу или наличию интерактивных атрибутов.


Ограничение области наблюдения

Наблюдение за document.body может быть дорогостоящим. Лучше отслеживать только контейнер интерфейса.

observer.observe(modalElement, {...});

Это существенно снижает количество мутаций.


Взаимодействие с модальными окнами

Одним из наиболее распространённых применений Tabbable является focus trapping — ограничение перемещения фокуса внутри модального окна.

MutationObserver используется для отслеживания:

  • добавления элементов в модальное окно;
  • изменения атрибутов;
  • удаления элементов.

Пример сценария:

  1. Открывается модальное окно.
  2. Вычисляется список tabbable-элементов.
  3. Внутри окна динамически загружается форма.
  4. MutationObserver обнаруживает изменения.
  5. Список tabbable обновляется.

Без наблюдения новые элементы не попадут в последовательность Tab.


Работа с компонентными фреймворками

В React, Vue или Svelte DOM часто обновляется через виртуальное дерево.

Пример:

  • компонент перерисовывается;
  • кнопка заменяется новой;
  • старый DOM-узел удаляется;
  • создаётся новый узел.

MutationObserver фиксирует эти изменения автоматически.

Это делает интеграцию Tabbable независимой от используемого фреймворка.


Отключение наблюдения

Когда наблюдение больше не требуется, observer необходимо отключить.

observer.disconnect();

Причины отключения:

  • удаление компонента;
  • закрытие модального окна;
  • переход на другую страницу в SPA.

Это предотвращает утечки памяти.


Повторное подключение наблюдателя

Иногда наблюдение временно отключается и включается снова.

Пример:

observer.disconnect();

updateTabbables();

observer.observe(container, config);

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


Практическая схема интеграции

Типичная архитектура работы MutationObserver вместе с Tabbable включает следующие элементы:

1. Контейнер интерфейса

const root = document.querySelector('#ui');

2. Функция вычисления tabbable

function compute() {
  return tabbable(root);
}

3. Наблюдатель

const observer = new MutationObserver(handleMutations);

4. Анализ изменений

function handleMutations(mutations) {
  for (const mutation of mutations) {
    if (mutation.type === "childList") {
      compute();
      break;
    }
  }
}

5. Запуск наблюдения

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

Такая схема обеспечивает автоматическую синхронизацию DOM и логики навигации по фокусу.


Ограничения MutationObserver

Несмотря на эффективность, механизм имеет ряд ограничений.

Асинхронность

MutationObserver не срабатывает мгновенно. Уведомления помещаются в очередь микрозадач и выполняются после завершения текущего JavaScript-цикла.

Это означает, что изменения DOM могут быть обработаны с небольшой задержкой.


Высокая частота событий

При массовых изменениях DOM может возникать большое количество мутаций.

Например:

  • генерация списка из сотен элементов;
  • рендер таблицы;
  • обновление интерфейса фреймворком.

Без оптимизации это может вызвать проблемы с производительностью.


Невозможность наблюдать вычисляемые стили

MutationObserver реагирует только на изменения DOM и атрибутов.

Он не отслеживает:

  • изменения CSS-правил;
  • изменения медиа-запросов;
  • изменения классов, если они не затрагивают отслеживаемые атрибуты.

В таких случаях состояние tabbable-элементов может измениться без уведомления наблюдателя.


Практика применения в системах доступности

В контексте accessibility MutationObserver играет важную роль:

  • поддерживает корректную навигацию клавиатурой;
  • обеспечивает актуальность списка фокусируемых элементов;
  • предотвращает «потерю» фокуса при динамических изменениях.

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

  • модальных окон;
  • сложных UI-панелей;
  • динамических форм;
  • SPA-приложений.

Наблюдение за изменениями DOM позволяет автоматически адаптировать систему фокусировки к текущему состоянию интерфейса.