Перейти к основному содержимому

Наблюдатели DOM — Intersection, Resize и Mutation

Разработчику Архитектору

Проблема scroll-слушателей

Раньше "элемент попал в экран" ловили через window.addEventListener('scroll', …) и getBoundingClientRect() на каждый кадр. Это:

  • нагружает главный поток;
  • плохо сочетается с вложенными прокручиваемыми контейнерами.

IntersectionObserver и ResizeObserver сообщают браузеру: "скажи, когда изменится видимость или размер" — без ручного опроса координат.

Связь: работа с HTML в JavaScript, виджеты, атрибут loading="lazy" у <img> — в HTML.


Observer API вместо опроса на scroll

На прототипе обработчик scroll кажется простым. На длинных каталогах и вложенной вёрстке постоянный пересчёт координат нагружает главный поток. IntersectionObserver и ResizeObserver переводят задачу в модель "вызови колбэк, когда изменилось пересечение или размер" — браузер сам определяет момент события.


IntersectionObserver

Следит, пересекается ли элемент с областью просмотра (viewport или заданный предок).

Код ITЗагрузка примера кода…

Разбор:

  • querySelectorAll('img[data-src]') выбирает только изображения, подготовленные для ленивой загрузки.
  • new IntersectionObserver((entries) => {...}, options) регистрирует колбэк, который вызывается при изменении видимости наблюдаемых элементов.
  • Проверка if (!entry.isIntersecting) continue; отбрасывает элементы, которые пока не попали в область наблюдения.
  • img.src = img.dataset.src переносит URL из data-src в рабочий src, запуская фактическую загрузку.
  • observer.unobserve(img) отключает наблюдение после успешной подгрузки и экономит ресурсы.
  • rootMargin: '200px 0px' стартует загрузку заранее, чтобы картинка успела прийти до появления на экране.

Поля entry

ПолеСмысл
isIntersectingесть ли пересечение сейчас
intersectionRatioдоля видимой площади (0–1)
targetнаблюдаемый элемент
boundingClientRectгеометрия элемента
rootBoundsграницы root

Бесконечный список

Наблюдайте сенсор внизу списка; при появлении — fetch следующей страницы и unobserve до вставки новых элементов.

Код ITЗагрузка примера кода…

Разбор:

  • sentinel служит триггером внизу списка: когда он появляется, пора грузить следующую порцию данных.
  • loading блокирует повторные параллельные запросы, пока текущая загрузка не завершена.
  • entries.some((e) => e.isIntersecting) учитывает пакетный характер observer-колбэков и проверяет факт видимости.
  • try/finally гарантирует сброс loading = false даже при ошибке в loadNextPage().
  • Такая схема даёт устойчивую бесконечную ленту без ручных вычислений скролла и высоты страницы.

Пауза видео и анимаций

Когда блок ушёл с экрана — остановить requestAnimationFrame или <video>:

const carousel = document.querySelector('.promo-carousel');

const visibilityObserver = new IntersectionObserver(
(entries) => {
const onScreen = entries[0]?.isIntersecting;
document.dispatchEvent(
new CustomEvent('carousel:visibility', { detail: { onScreen } })
);
},
{ threshold: 0.25 }
);

visibilityObserver.observe(carousel);

Разбор:

  • Observer отслеживает видимость блока карусели и превращает её в событие приложения.
  • entries[0]?.isIntersecting безопасно читает состояние первой записи даже при редких случаях пустого массива.
  • new CustomEvent('carousel:visibility', { detail: { onScreen } }) публикует понятный сигнал для других модулей.
  • Порог threshold: 0.25 считает блок "видимым", когда в viewport присутствует минимум четверть площади.
  • Подход отделяет детекцию видимости от бизнес-логики и облегчает расширение компонента.

root — прокрутка внутри контейнера

const scrollBox = document.querySelector('.modal__body');
const io = new IntersectionObserver(callback, { root: scrollBox });

Разбор:

  • root: scrollBox переключает систему координат с окна браузера на прокручиваемый контейнер модалки.
  • В таком режиме событие зависит от видимости элемента внутри конкретного блока, а не всей страницы.
  • Этот приём полезен для внутренних списков, чатов и виртуализированных таблиц в компонентах.

ResizeObserver

Реагирует на изменение размеров элемента (не окна). Удобно для графиков, canvas, текстовых блоков с переносом.

const chartHost = document.querySelector('#chart');

const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentBoxSize?.[0] ?? entry.contentRect;
redrawChart(width, height);
}
});

resizeObserver.observe(chartHost);

// при destroy виджета:
resizeObserver.disconnect();

Разбор:

  • ResizeObserver отслеживает изменение размеров элемента, а не глобального окна.
  • entry.contentBoxSize?.[0] ?? entry.contentRect учитывает разные форматы данных в браузерах.
  • Вызов redrawChart(width, height) пересчитывает график только тогда, когда это действительно нужно.
  • disconnect() освобождает наблюдатель при уничтожении компонента и предотвращает лишние колбэки.
  • Это основной паттерн для адаптивных графиков и canvas-компонентов в динамичной вёрстке. Подробнее про отрисовку — Canvas 2D.

Отличие от window.resize — срабатывает, когда меняется сам блок (flex, sidebar, @container).


MutationObserver

MutationObserver сообщает, когда в поддереве добавили, удалили или изменили узлы и атрибуты. Колбэк попадает в очередь микрозадач (как Promise.then) — см. event loop.

const host = document.getElementById('chat-messages');

const mo = new MutationObserver((records) => {
for (const record of records) {
if (record.type === 'childList' && record.addedNodes.length) {
host.scrollTop = host.scrollHeight;
}
}
});

mo.observe(host, { childList: true, subtree: true });

Разбор:

  • MutationObserver получает список изменений DOM и позволяет реагировать на них централизованно.
  • Условие record.type === 'childList' && record.addedNodes.length фильтрует события до полезного кейса "в чат добавили сообщение".
  • host.scrollTop = host.scrollHeight прокручивает контейнер к последнему сообщению автоматически.
  • Опции childList: true, subtree: true включают наблюдение за добавлением/удалением узлов во всём поддереве.
  • Такой подход особенно полезен, когда DOM модифицируется сторонним кодом без ваших явных хуков.
Опция observeЧто отслеживать
childListдобавление/удаление дочерних узлов
attributesсмена атрибутов (class, data-*, disabled…)
characterDataтекст внутри текстовых узлов
subtreeизменения во всём потомке, не только прямых детей
attributeFilterтолько перечисленные атрибуты

Типичные задачи:

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

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

  • не заменяет события (click, input) — для реакции на действия пользователя нужны обработчики;
  • частые мутации (анимация, посимвольный ввод) могут вызвать лавину колбэков — сужайте subtree и фильтры;
  • при destroy() виджета вызывайте disconnect(), как у других observers.

Сравнение с альтернативами

ЗадачаРекомендация
Ленивые картинкиloading="lazy" + при необходимости IO для фона/data-src
"В viewport"IntersectionObserver
Размер блока для layout JSResizeObserver
Изменилась структура DOMMutationObserver
Позиция при каждом scrollизбегать; IO или CSS position: sticky

Очистка

Наблюдатели держат ссылки на элементы. При удалении узла из DOM:

observer.unobserve(element);
// или полностью:
observer.disconnect();

Разбор:

  • unobserve(element) точечно снимает наблюдение с одного элемента и сохраняет observer для остальных.
  • disconnect() полностью отключает observer и очищает все подписки разом.
  • Эти методы обычно вызывают в destroy() вместе с очисткой таймеров и обработчиков событий.
  • Явная очистка важна для долгоживущих SPA, где компоненты часто монтируются и удаляются.

В методе destroy() виджета отключайте все observers вместе с таймерами и отменой fetch.


Типичные архитектурные шаблоны

ШаблонЧто наблюдаемРезультат
Ленивая галереяIntersectionObserver на img[data-src]загрузка только близких к экрану картинок
Адаптивный графикResizeObserver на контейнерепересчёт canvas при изменении сетки
Интеграция со сторонним чатомMutationObserver на контейнере сообщенийавтоскролл и синхронизация счётчиков
Длинный каталогIntersectionObserver на нижнем sentinelбесконечная подгрузка порциями

В большинстве интерфейсов наблюдатели работают вместе. Это даёт реакцию на реальное изменение состояния страницы вместо постоянного опроса.


Частые ошибки внедрения

  • Один observer создаётся на каждый элемент без нужды, что раздувает память. Для однотипных задач обычно хватает одного экземпляра на группу элементов.
  • В IntersectionObserver ставится threshold: 1, и элементы редко считаются видимыми из-за частичного перекрытия.
  • В MutationObserver включают subtree: true на весь document.body, после чего приходит слишком много записей.
  • При размонтировании компонента забывают disconnect(), и колбэки продолжают отрабатывать на удалённом UI.

Краткий итог

IntersectionObserver — видимость и ленивая подгрузка. ResizeObserver — пересчёт layout при изменении размеров контейнера. MutationObserver — реакция на правки дерева без ручного опроса. Все три снижают нагрузку по сравнению со scroll- и DOM-опросом в цикле и хорошо стыкуются с ванильными компонентами.