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

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

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

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

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

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

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

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


IntersectionObserver

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

const images = document.querySelectorAll('img[data-src]');

const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;

const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
observer.unobserve(img);
}
},
{
root: null, // viewport
rootMargin: '200px 0px', // подгрузить чуть раньше появления
threshold: 0.01, // доля видимой площади (0…1 или массив)
}
);

images.forEach((img) => observer.observe(img));

Поля entry

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

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

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

const sentinel = document.querySelector('#list-sentinel');
let loading = false;

const listObserver = new IntersectionObserver(async (entries) => {
const visible = entries.some((e) => e.isIntersecting);
if (!visible || loading) return;

loading = true;
try {
const chunk = await loadNextPage();
appendItems(chunk);
} finally {
loading = false;
}
});

listObserver.observe(sentinel);

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

Когда блок ушёл с экрана — остановить 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);

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

const scrollBox = document.querySelector('.modal__body');
const io = new IntersectionObserver(callback, { 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();

Отличие от 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 });
Опция 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();

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


Краткий итог

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


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).