Пошук уроків, статей та іншого контенту
Використовуватимете IntersectionObserver і ResizeObserver для реакції на зміни видимості та розміру.
У браузері часто потрібно реагувати на зміни, які відбуваються незалежно від подій користувача:
елемент з’явився у видимій області;
елемент перестав бути видимим;
розмір елемента змінився через адаптивну верстку;
вміст елемента змінився і вплинув на його ширину або висоту.
Для таких задач JavaScript надає спеціальні API:
IntersectionObserver — спостерігає за перетином елемента з іншою областю, зазвичай із viewport;
ResizeObserver — спостерігає за зміною розміру елемента.
Обидва API працюють асинхронно та ефективніші за постійні перевірки через scroll, resize або setInterval.
IntersectionObserver повідомляє, коли спостережуваний елемент перетинається з областю спостереження.
Найчастіше областю спостереження є viewport браузера. Наприклад, можна:
завантажувати зображення лише перед появою на екрані;
запускати анімацію під час появи секції;
реалізувати нескінченне прокручування;
визначати активний розділ сторінки;
відстежувати видимість реклами або іншого контенту.
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
console.log("Елемент перетнув область:", entry.isIntersecting);
}
});Callback отримує масив об’єктів IntersectionObserverEntry. Один callback може бути викликаний для кількох елементів.
Щоб почати спостереження, потрібно передати елемент у метод observe:
const element = document.querySelector(".card");
observer.observe(element);Щоб припинити спостереження:
observer.unobserve(element);Щоб припинити спостереження за всіма елементами:
observer.disconnect();У callback корисними є такі властивості:
entry.target — елемент, за яким спостерігають;
entry.isIntersecting — true, якщо елемент перетинає область спостереження;
entry.intersectionRatio — частка видимої області елемента від 0 до 1;
entry.boundingClientRect — прямокутник елемента;
entry.intersectionRect — частина елемента, яка перетинається з областю;
entry.rootBounds — прямокутник області спостереження.
Наприклад:
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("visible");
} else {
entry.target.classList.remove("visible");
}
});
});
document.querySelectorAll(".section").forEach((section) => {
observer.observe(section);
});Другим аргументом конструктора є об’єкт налаштувань:
const observer = new IntersectionObserver(callback, options);rootВизначає область спостереження.
За замовчуванням:
root: nullЦе означає viewport браузера.
Якщо потрібно спостерігати за елементом усередині прокручуваного контейнера, можна вказати цей контейнер:
const list = document.querySelector(".list");
const observer = new IntersectionObserver(callback, {
root: list
});Контейнер зазвичай повинен мати обмежену висоту та overflow: auto або overflow: scroll.
rootMarginДодає відступ до області спостереження. Значення записується подібно до CSS-властивості margin:
const observer = new IntersectionObserver(callback, {
rootMargin: "200px 0px"
});У цьому прикладі елемент буде вважатися таким, що наближається до viewport, ще за 200 пікселів до фактичної появи. Це зручно для попереднього завантаження даних або зображень.
thresholdВизначає, яка частка елемента має бути видимою, щоб callback було викликано.
const observer = new IntersectionObserver(callback, {
threshold: 0.5
});Значення 0.5 означає, що callback спрацює, коли видимою стане половина елемента.
Можна передати масив порогів:
const observer = new IntersectionObserver(callback, {
threshold: [0, 0.25, 0.5, 0.75, 1]
});Тоді спостерігач реагуватиме на проходження кожного з цих порогів.
ResizeObserver повідомляє про зміну розміру елемента. Він реагує на зміни ширини або висоти, навіть якщо вони сталися не через зміну розміру вікна.
Це важливо, тому що window.resize не повідомляє безпосередньо про зміну розміру кожного елемента.
Приклад створення:
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
console.log("Розмір елемента змінився:", entry.contentRect);
}
});Початок і завершення спостереження мають такі самі методи:
resizeObserver.observe(element);
resizeObserver.unobserve(element);
resizeObserver.disconnect();Для кожного елемента callback отримує ResizeObserverEntry.
Найчастіше використовують:
entry.target — елемент, розмір якого змінився;
entry.contentRect — прямокутник області вмісту;
entry.contentBoxSize — розмір content box;
entry.borderBoxSize — розмір border box, якщо підтримується браузером.
Для простих задач достатньо contentRect:
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const width = entry.contentRect.width;
if (width < 500) {
entry.target.classList.add("compact");
} else {
entry.target.classList.remove("compact");
}
}
});Другий аргумент методу observe може вказати, який розмір спостерігати:
observer.observe(element, {
box: "border-box"
});Можливі значення:
"content-box" — розмір області вмісту;
"border-box" — розмір елемента разом із padding і border;
"device-pixel-content-box" — розмір content box у фізичних пікселях, якщо браузер це підтримує.
Якщо параметр не вказано, зазвичай використовується "content-box".
Нижче наведено повний приклад HTML. Він:
додає клас до карток, коли вони з’являються у viewport;
завантажує умовний вміст лише під час появи картки;
змінює підпис залежно від ширини картки;
використовує ResizeObserver для відстеження розміру.
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IntersectionObserver і ResizeObserver</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 24px;
font-family: sans-serif;
background: #f1f5f9;
}
.spacer {
height: 70vh;
display: grid;
place-items: center;
color: #64748b;
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
max-width: 900px;
margin: 0 auto;
}
.card {
min-height: 180px;
padding: 20px;
border: 2px solid #cbd5e1;
border-radius: 12px;
background: white;
opacity: 0.2;
transform: translateY(30px);
transition:
opacity 400ms ease,
transform 400ms ease,
border-color 200ms ease;
}
.card.visible {
opacity: 1;
transform: translateY(0);
border-color: #2563eb;
}
.card__status {
margin-top: 16px;
color: #475569;
font-size: 14px;
}
.card.wide {
border-color: #16a34a;
}
</style>
</head>
<body>
<div class="spacer">
Прокрутіть сторінку вниз
</div>
<main class="cards">
<article class="card">
<h2>Картка 1</h2>
<p>Ця картка з’явиться з анімацією.</p>
<p class="card__status">Очікування...</p>
</article>
<article class="card">
<h2>Картка 2</h2>
<p>Її розмір відстежує ResizeObserver.</p>
<p class="card__status">Очікування...</p>
</article>
<article class="card">
<h2>Картка 3</h2>
<p>Спробуйте змінити ширину вікна.</p>
<p class="card__status">Очікування...</p>
</article>
</main>
<script>
const cards = document.querySelectorAll(".card");
const visibilityObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
const card = entry.target;
const status = card.querySelector(".card__status");
card.classList.add("visible");
status.textContent = "Картка видима";
// Після першої появи картки більше не потрібно її перевіряти.
observer.unobserve(card);
});
},
{
root: null,
rootMargin: "0px 0px -10% 0px",
threshold: 0.25
}
);
const sizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const card = entry.target;
const status = card.querySelector(".card__status");
const width = Math.round(entry.contentRect.width);
if (width >= 400) {
card.classList.add("wide");
status.textContent = `Широка картка: ${width}px`;
} else {
card.classList.remove("wide");
status.textContent = `Компактна картка: ${width}px`;
}
});
});
cards.forEach((card) => {
visibilityObserver.observe(card);
sizeObserver.observe(card);
});
// У реальному застосунку це може бути очищення під час знищення компонента.
window.addEventListener("beforeunload", () => {
visibilityObserver.disconnect();
sizeObserver.disconnect();
});
</script>
</body>
</html>Зображенню можна додати власний атрибут із URL, а значення src встановити лише перед появою:
<img
class="lazy-image"
width="640"
height="360"
alt="Пейзаж"
data-src="landscape.jpg">const imageObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
const image = entry.target;
image.src = image.dataset.src;
image.removeAttribute("data-src");
observer.unobserve(image);
});
},
{
rootMargin: "300px 0px"
}
);
document.querySelectorAll(".lazy-image").forEach((image) => {
imageObserver.observe(image);
});rootMargin дозволяє почати завантаження трохи раніше, щоб зображення встигло завантажитися до появи на екрані.
Можна створити невидимий елемент-індикатор внизу списку та завантажувати наступну порцію даних, коли він стає видимим:
const sentinel = document.querySelector(".sentinel");
const loadObserver = new IntersectionObserver(
async (entries) => {
const [entry] = entries;
if (!entry.isIntersecting) {
return;
}
loadObserver.unobserve(sentinel);
await loadNextPage();
loadObserver.observe(sentinel);
},
{
rootMargin: "200px"
}
);
loadObserver.observe(sentinel);Важливо тимчасово припиняти спостереження або контролювати стан завантаження, щоб не запустити кілька однакових запитів одночасно.
CSS media queries реагують на ширину viewport. Але іноді компонент може перебувати в бічній панелі, модальному вікні або grid-комірці. У такому разі важлива ширина саме компонента.
const panels = document.querySelectorAll(".panel");
const panelObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const panel = entry.target;
const isCompact = entry.contentRect.width < 360;
panel.classList.toggle("panel--compact", isCompact);
});
});
panels.forEach((panel) => {
panelObserver.observe(panel);
});Під час роботи з динамічним DOM важливо правильно керувати спостерігачами.
observeПочинає спостереження за одним елементом:
observer.observe(element);Один спостерігач може контролювати багато елементів.
unobserveПрипиняє спостереження лише за конкретним елементом:
observer.unobserve(element);Це корисно, коли елемент потрібно перевірити тільки один раз.
disconnectПовністю припиняє спостереження за всіма елементами:
observer.disconnect();Цей метод слід викликати, коли компонент видаляється зі сторінки або більше не потребує спостереження.
entry.isIntersecting без перевіркиCallback може викликатися і під час виходу елемента з області видимості. Якщо код має виконуватися лише під час появи, потрібна перевірка:
if (entry.isIntersecting) {
// Дія лише для видимого елемента
}unobserveЯкщо потрібно виконати дію лише один раз, після цього варто припинити спостереження:
if (entry.isIntersecting) {
initializeComponent(entry.target);
observer.unobserve(entry.target);
}Інакше callback продовжить викликатися під час наступних перетинів.
rootЯкщо елемент знаходиться в прокручуваному контейнері, але root залишено null, спостерігач перевірятиме перетин із viewport, а не з контейнером.
const container = document.querySelector(".scroll-container");
const observer = new IntersectionObserver(callback, {
root: container
});Для великих або динамічних елементів значення threshold: 1 може бути незручним: елемент повинен повністю потрапити в область спостереження. Для більшості сценаріїв достатньо 0, 0.25 або 0.5.
Не слід безпосередньо змінювати розмір елемента в callback так, щоб це знову викликало зміну розміру:
const observer = new ResizeObserver((entries) => {
entries.forEach((entry) => {
// Небезпечний підхід: зміна ширини може викликати нову зміну розміру.
entry.target.style.width = `${entry.contentRect.width + 1}px`;
});
});Такий код може спричинити нескінченний цикл або помилку ResizeObserver loop completed with undelivered notifications.
Краще:
змінювати клас, а не встановлювати розмір на кожному виклику;
перевіряти, чи справді значення змінилося;
не змінювати розмір елемента без необхідності.
Callback може викликатися часто, особливо коли на сторінці багато елементів. Не варто виконувати там важкі обчислення або часті мережеві запити без контролю стану.
За потреби можна:
обробляти лише потрібні елементи;
відписуватися після одноразової дії;
об’єднувати оновлення через requestAnimationFrame;
не запускати повторний запит, якщо попередній ще триває.
Спостерігачі працюють асинхронно. Не слід припускати, що callback буде виконано одразу після зміни DOM або стилів. Якщо послідовність дій важлива, стан потрібно явно зберігати у змінних або атрибутах елемента.
Використовуйте IntersectionObserver, якщо потрібно дізнатися:
чи видно елемент;
коли елемент наблизився до viewport;
коли користувач дійшов до кінця списку;
коли запускати анімацію появи.
Використовуйте ResizeObserver, якщо потрібно дізнатися:
чи змінилася ширина або висота елемента;
коли компонент потрібно перемкнути між компактним і розширеним режимом;
коли розмір залежить від батьківського контейнера, а не від viewport;
коли зміна вмісту впливає на геометрію компонента.
Обидва спостерігачі можуть використовуватися разом. Наприклад, компонент можна активувати лише після появи на екрані, а його внутрішній інтерфейс адаптувати до поточної ширини.
IntersectionObserver відстежує перетин елемента з viewport або заданим контейнером.
ResizeObserver відстежує зміни ширини та висоти елемента.
Callback спостерігачів отримує масив записів, тому потрібно перебирати entries.
Для IntersectionObserver важливі root, rootMargin і threshold.
Для одноразових дій використовуйте unobserve.
Для повного очищення використовуйте disconnect.
ResizeObserver не варто використовувати для циклічної зміни власного розміру елемента.
Спостерігачі допомагають ефективно реалізувати ліниве завантаження, нескінченне прокручування, анімації та адаптивні компоненти.