Um listener de scroll em window pode disparar centenas de vezes durante um único swipe. Se você executa getBoundingClientRect() em cada card de um feed a cada um desses eventos, cinquenta imagens já bastam para a página engasgar. A Intersection Observer API tira essa verificação do caminho do scroll: você registra quais elementos observar, e o navegador avisa quando a visibilidade deles muda de fato.
Como o IntersectionObserver funciona
Um observer vigia um ou mais elementos target em relação a um root (o viewport, por padrão) e chama um callback sempre que um target cruza um limite de visibilidade.
| |
Cada entry traz isIntersecting (um booleano), intersectionRatio (quanto do target está dentro do root, de 0 a 1) e o próprio boundingClientRect do target. O callback é assíncrono. O navegador calcula as interseções uma vez por frame, como parte do próprio trabalho de renderização, então seu código nunca força um recálculo de layout enquanto o usuário rola a página.
threshold também aceita um array. [0, 0.5, 1] dispara o callback separadamente em 0%, 50% e 100% de visibilidade, a forma de enviar um evento “marcado como lido na metade do scroll” sem criar três observers.
Como fazer lazy loading de imagens com IntersectionObserver
O caso de uso clássico: não baixar uma imagem até que ela esteja prestes a entrar no viewport.
| |
| |
rootMargin é o que faz tudo isso parecer instantâneo. Um valor positivo aumenta a caixa efetiva do root, então o callback dispara enquanto a imagem ainda está 200px abaixo da dobra, em vez de só no primeiro pixel visível. Mantenha também width e height explícitos na tag: eles reservam o espaço para que o layout não salte quando a imagem real chegar, uma causa separada de layout shift que a maioria das demos de lazy loading ignora.
Antes de escrever tudo isso, verifique se <img loading="lazy"> já basta. Todo navegador atual já suporta isso nativamente, sem precisar de JavaScript. A versão com observer se justifica quando você precisa de algo que o atributo não faz: um swap de placeholder personalizado, vídeo ou iframes lazy, ou imagens de fundo definidas via CSS.
Como construir scroll infinito com um elemento sentinela
O scroll infinito não observa o conteúdo já carregado. Ele observa um elemento vazio abaixo da lista e carrega mais quando essa sentinela fica visível.
| |
| |
O par unobserve/observe existe por causa de redes lentas. Sem ele, um usuário que rola rápido o suficiente pode disparar o callback de novo antes que a primeira requisição seja resolvida, e duas páginas de resultados chegam fora de ordem.
loadNextPage é uma chamada fetch comum, então precisa da mesma verificação de response.ok que qualquer outra requisição. O guia sobre tratamento de erros da Fetch API cobre o que fazer quando essa requisição responde 404 ou a rede cai no meio do scroll. Em caso de falha, deixe a sentinela sem observação e mostre um botão de nova tentativa. Voltar a observá-la na hora te coloca em um loop de requisições que falham.
A forma antiga de fazer isso era um handler de scroll com throttle ou debounce chamando getBoundingClientRect() na sentinela. Como mostra a comparação entre debounce e throttle, as duas técnicas continuam fazendo polling, só que com menos frequência. Um observer não faz polling nenhum. O navegador é quem faz a verificação, e seu código reage ao resultado.
Quando o callback do IntersectionObserver é executado
Não na microtask queue. Se você já leu como o async/await interage com o event loop, não assuma que o callback se comporta como a continuação de uma promise. O navegador o agenda a partir do próprio pipeline de renderização, perto do paint. É por isso que ele é leve o suficiente para uma página com scroll, e também por isso que é a ferramenta errada para lógica que precisa rodar antes do próximo repaint.
Armadilhas comuns do IntersectionObserver
Ele testa uma bounding box, não pixels. Um elemento reportado como 100% intersecting ainda pode estar completamente escondido atrás de um modal opaco ou de um header sticky pintado em cima. A API responde “essa caixa está dentro daquela caixa”, não “o usuário consegue ver isso”.
Um observer por elemento se acumula. Chamar new IntersectionObserver() para cada um de duzentos cards cria duzentos observers separados. Crie um por comportamento (um para todas as imagens lazy, um para a sentinela) e chame .observe() em cada target que precisar. O callback já recebe entries de qualquer target que tenha mudado.
O jsdom não implementa isso. Em uma suíte Jest ou Vitest rodando em jsdom, IntersectionObserver é undefined a menos que você o mocke ou instale um polyfill. Mocke o construtor e chame o callback você mesmo com entries falsas para testar a lógica de lazy loading isoladamente. Não tente simular um scroll real em um teste unitário.
Observar antes do elemento existir. Em um framework que renderiza de forma assíncrona, chamar observer.observe(el) em uma ref ainda null falha silenciosamente: sem erro, sem warning, o elemento nunca é observado. Observe dentro do hook que roda depois que o nó do DOM foi montado (useEffect no React, mounted no Vue).
Quando usar IntersectionObserver em vez de loading=“lazy”
Para tags <img> simples, adicione loading="lazy" e não escreva JavaScript nenhum. Use IntersectionObserver para o que um atributo não consegue expressar: gatilhos de paginação, autoplay de vídeo ao ficar visível, eventos de “visto” para analytics, swaps de placeholder para imagem. Um bom primeiro passo é achar no seu código o listener de scroll que chama getBoundingClientRect() e substituí-lo por um único observer compartilhado.