API Intersection Observer do JavaScript
Aprenda a API Intersection Observer do JavaScript para detectar eficientemente quando um elemento entra ou sai do viewport — para lazy loading, scroll infinito e animações.
A API Intersection Observer permite que você solicite ao navegador que te avise quando um elemento entra ou sai da parte visível da página. Ela faz isso de forma eficiente e assíncrona, sem o custo de desempenho de monitorar eventos de scroll por conta própria. É a ferramenta certa para lazy-loading de imagens, construção de scroll infinito, acionamento de animações conforme o conteúdo aparece e para medir se um anúncio ou banner foi realmente visto.
O Problema que Ela Resolve
Antes desta API existir, responder à simples pergunta "este elemento está na tela agora?" era surpreendentemente trabalhoso. Era preciso adicionar um listener ao evento scroll (e frequentemente resize), depois chamar getBoundingClientRect() em cada elemento rastreado para comparar sua posição com o viewport.
// The old, expensive way — runs on every scroll tick.
window.addEventListener('scroll', () => {
const rect = element.getBoundingClientRect();
const inView = rect.top < window.innerHeight && rect.bottom > 0;
if (inView) {
// do something
}
});Eventos de scroll disparam dezenas de vezes por segundo, e getBoundingClientRect() força o navegador a recalcular o layout (um "reflow"). Fazer esse trabalho de forma síncrona na thread principal durante um scroll é uma fonte clássica de jank. (Veja Tratamento de Eventos no DOM e Scroll em JavaScript para entender como esses eventos se comportam.)
IntersectionObserver inverte o modelo. Em vez de você consultar posições, o navegador monitora os elementos por você e faz o callback somente quando a visibilidade de fato muda. O trabalho acontece fora da thread principal, portanto não bloqueia o scroll. Para saber mais sobre por que isso importa, leia Otimização de Desempenho do DOM. É um irmão próximo da API MutationObserver, que monitora mudanças na estrutura do DOM em vez de visibilidade.
Uso Básico
Você cria um observer com um callback e, em seguida, informa quais elementos ele deve monitorar com observe().
// 1. Create an observer with a callback and (optional) options.
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
console.log('Element is now visible:', entry.target);
} else {
console.log('Element left the viewport:', entry.target);
}
});
});
// 2. Start watching a target element.
const target = document.querySelector('#box');
observer.observe(target);O callback recebe um array de entradas, uma por elemento observado cuja visibilidade mudou. Um único observer pode monitorar muitos elementos, e esse é o padrão recomendado — crie um observer e chame observe() para cada alvo, em vez de criar um observer por elemento.
O callback é executado de forma assíncrona e as mudanças são agrupadas em lote — o navegador pode reportar várias entradas em uma única chamada. Ele também dispara uma vez logo após você começar a observar, para que você obtenha o estado de visibilidade inicial do elemento sem esperar por um scroll. O suporte nos navegadores modernos é excelente.
Configurando o Observer
O segundo argumento para o construtor é um object de opções com três propriedades.
root
O elemento usado como viewport para verificar a visibilidade. O alvo deve ser descendente do root. Quando root é null (o padrão), o próprio viewport do navegador é utilizado.
const observer = new IntersectionObserver(callback, {
root: document.querySelector('#scroll-container'),
});rootMargin
Uma margem ao redor do root, escrita como um valor de margin do CSS. Ela aumenta ou reduz a caixa usada para verificações de interseção. Um truque comum é usar uma margem inferior positiva para que os elementos sejam reportados como "visíveis" antes de realmente entrarem na tela — útil para carregar conteúdo antecipadamente.
const observer = new IntersectionObserver(callback, {
// Trigger 200px before the element reaches the bottom edge.
rootMargin: '0px 0px 200px 0px',
});threshold
Um número de 0 a 1, ou um array de números, indicando ao observer em quais proporções de visibilidade ele deve disparar. 0 significa "disparar assim que um único pixel estiver visível", 1 significa "disparar apenas quando o elemento estiver totalmente visível". Um array dispara em cada proporção listada.
const observer = new IntersectionObserver(callback, {
// Fire at 0%, 50%, and 100% visibility.
threshold: [0, 0.5, 1],
});O que Há Dentro de uma Entrada
Cada object no array entries descreve a visibilidade de um elemento no momento em que o callback foi executado. As propriedades mais úteis são:
isIntersecting— um boolean:truese o elemento está atualmente visível dentro do root.intersectionRatio— quanto do elemento está visível, de0a1.target— o elemento sendo observado.boundingClientRect— o tamanho e a posição do alvo.intersectionRect— a parte visível do alvo.rootBounds— o retângulo do root (ajustado porrootMargin).time— um timestamp de quando a mudança foi registrada.
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
console.log(entry.target.id, 'visible:', entry.isIntersecting);
console.log('ratio:', entry.intersectionRatio.toFixed(2));
}
});Métodos: observe, unobserve, disconnect
Uma instância do observer fornece três métodos:
observe(element)— começa a monitorar um elemento.unobserve(element)— para de monitorar um elemento específico.disconnect()— para de monitorar todos os elementos de uma vez.
Uma boa prática importante: quando um elemento concluiu sua tarefa pontual — por exemplo, uma imagem que terminou o lazy-loading — chame unobserve() nele para que o navegador pare de rastrear algo que nunca mudará novamente.
Caso de Uso 1 — Lazy Loading de Imagens
O lazy loading adia o download de imagens até que estejam prestes a ser vistas. Coloque a URL real em um atributo data-src, observe cada imagem e transfira-a para src quando ela se tornar visível — depois pare de observá-la.
<img data-src="photo-1.jpg" alt="First photo" width="600" height="400" />
<img data-src="photo-2.jpg" alt="Second photo" width="600" height="400" />
<img data-src="photo-3.jpg" alt="Third photo" width="600" height="400" />const images = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const img = entry.target;
img.src = img.dataset.src; // load the real image
img.removeAttribute('data-src');
observer.unobserve(img); // job done — stop watching it
});
}, { rootMargin: '0px 0px 200px 0px' }); // start loading a little early
images.forEach((img) => imageObserver.observe(img));Navegadores modernos também suportam o atributo nativo loading="lazy" em <img> e <iframe>, que não requer nenhum JavaScript. Use IntersectionObserver quando precisar de comportamento personalizado — troca de placeholder, fade-in ou carregamento de conteúdo que não sejam imagens.
Caso de Uso 2 — Scroll Infinito
Para scroll infinito, coloque um elemento "sentinela" vazio no final da lista. Quando esse sentinela entrar no viewport, carregue a próxima página de dados e adicione-a. Como o sentinela permanece no final, o mesmo observer continua disparando conforme o usuário rola mais.
<ul id="list"></ul>
<div id="sentinel"></div>const list = document.querySelector('#list');
const sentinel = document.querySelector('#sentinel');
let page = 1;
let loading = false;
async function loadMore() {
if (loading) return; // guard against overlapping loads
loading = true;
const res = await fetch('/api/items?page=' + page);
const items = await res.json();
items.forEach((item) => {
const li = document.createElement('li');
li.textContent = item.title;
list.appendChild(li);
});
page += 1;
loading = false;
}
const scrollObserver = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
loadMore();
}
});
scrollObserver.observe(sentinel);Caso de Uso 3 — Animações Reveladas ao Rolar
Um efeito popular é fazer elementos aparecerem com fade ou deslizamento conforme entram no viewport. Mantenha a animação no CSS e deixe o JavaScript adicionar uma classe no momento certo.
.reveal {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal.is-visible {
opacity: 1;
transform: translateY(0);
}const revealItems = document.querySelectorAll('.reveal');
const revealObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target); // animate only once
}
});
}, { threshold: 0.15 }); // fire when ~15% is showing
revealItems.forEach((el) => revealObserver.observe(el));Caso de Uso 4 — Rastreamento de Impressões / Visibilidade
Ferramentas de analytics frequentemente precisam saber se o conteúdo foi realmente visto, não apenas presente no DOM. Um threshold mais alto permite registrar uma impressão somente quando uma parte significativa de um elemento está visível.
const adObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.intersectionRatio >= 0.5) {
sendImpression(entry.target.dataset.adId);
adObserver.unobserve(entry.target); // count each ad once
}
});
}, { threshold: 0.5 }); // at least 50% visible
document.querySelectorAll('.ad').forEach((ad) => adObserver.observe(ad));Você pode estender isso com um timer para exigir, por exemplo, um segundo completo de 50% de visibilidade antes de contar uma impressão — um padrão comum para anúncios "visíveis".
Resumo
A API Intersection Observer substitui listeners de scroll frágeis e que consomem muito desempenho por uma forma limpa e assíncrona de reagir à visibilidade dos elementos. Crie um observer, aponte-o para seus alvos com observe(), leia isIntersecting e intersectionRatio no callback e chame unobserve() quando o trabalho de um elemento estiver concluído. Com root, rootMargin e threshold você pode ajustar exatamente quando ele dispara — tornando lazy loading, scroll infinito, animações de scroll e rastreamento de impressões simples e eficientes.