Наблюдатели 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 JS | ResizeObserver |
| Изменилась структура DOM | MutationObserver |
| Позиция при каждом 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-опросом в цикле и хорошо стыкуются с ванильными компонентами.