Como usar ResizeObserver e IntersectionObserver para UX responsiva
1. Introdução aos Observadores Nativos do Navegador
Medir o tamanho de elementos ou detectar quando um item entra na tela sempre foi um desafio para desenvolvedores front-end. Antes dos observadores nativos, a abordagem comum era usar polling com setInterval ou escutar eventos como scroll e resize — soluções que consomem recursos desnecessários e degradam a performance.
A plataforma web moderna oferece três observadores especializados:
- ResizeObserver: notifica quando as dimensões de um elemento mudam
- IntersectionObserver: detecta quando um elemento entra ou sai da viewport
- MutationObserver: monitora mudanças na árvore DOM (não abordado neste artigo)
Para UX responsiva, os dois primeiros são essenciais. Eles permitem implementar lazy loading, layouts adaptativos e animações condicionais sem bibliotecas externas, com performance otimizada.
2. ResizeObserver: Monitorando Mudanças de Tamanho em Elementos
A API é simples: criamos uma instância passando um callback, e chamamos observe() no elemento alvo.
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
console.log(`Elemento redimensionado: ${width} x ${height}`);
}
});
const container = document.querySelector('.meu-container');
observer.observe(container);
O callback recebe um array de ResizeObserverEntry. Cada entrada contém contentRect (com width, height, top, left) e borderBoxSize (que inclui bordas e padding, útil para cálculos mais precisos).
Exemplo prático: gráfico que se ajusta automaticamente
const chartContainer = document.getElementById('chart');
const resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
const { width } = entry.contentRect;
// Redesenha o gráfico com a nova largura
renderChart(width);
}
});
resizeObserver.observe(chartContainer);
Isso elimina a necessidade de recalcular o layout manualmente quando o usuário redimensiona a janela.
3. IntersectionObserver: Detectando Visibilidade e Entrada na Viewport
O IntersectionObserver permite saber quando um elemento cruza o limite da viewport (ou de outro elemento raiz). A configuração básica:
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('Elemento visível na viewport');
}
});
}, {
threshold: 0.5, // Dispara quando 50% do elemento está visível
rootMargin: '0px'
});
const elemento = document.querySelector('.alvo');
observer.observe(elemento);
Parâmetros importantes:
threshold: valor entre 0 e 1 (ou array de valores) que define a proporção de visibilidade para disparar o callbackrootMargin: margem ao redor do root (viewport por padrão), útil para pré-carregar conteúdo antes da entrada
Exemplo prático: lazy loading de imagens
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // Carrega a imagem real
img.classList.remove('lazy');
observer.unobserve(img); // Para de observar após carregar
}
});
}, {
rootMargin: '200px', // Começa o carregamento 200px antes
threshold: 0.1
});
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img);
});
Isso melhora a performance percebida e economiza banda, carregando imagens apenas quando necessário.
4. Combinando Observadores para UX Responsiva Avançada
A verdadeira potência surge quando combinamos os dois observadores em um mesmo componente.
Caso real: grid de cards adaptativo
const cardGrid = document.getElementById('card-grid');
const cards = document.querySelectorAll('.card');
// Observa mudanças de tamanho no grid
const resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
const { width } = entry.contentRect;
if (width < 600) {
cardGrid.classList.add('grid-compact');
} else {
cardGrid.classList.remove('grid-compact');
}
}
});
resizeObserver.observe(cardGrid);
// Observa visibilidade para animar cards ao entrar na tela
const visibilityObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('card-animado');
visibilityObserver.unobserve(entry.target);
}
});
}, { threshold: 0.3 });
cards.forEach(card => visibilityObserver.observe(card));
Neste exemplo, o grid se adapta à largura disponível (como container queries) e os cards animam individualmente conforme aparecem na rolagem.
Pausando vídeos fora da tela:
const videoObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
const video = entry.target;
if (entry.isIntersecting) {
video.play();
} else {
video.pause();
}
});
}, { threshold: 0.5 });
document.querySelectorAll('video').forEach(v => videoObserver.observe(v));
Isso economiza bateria e processamento, especialmente em dispositivos móveis.
5. Performance e Boas Práticas com Observadores
Observadores são eficientes por natureza, mas algumas práticas evitam problemas:
Evitar observadores aninhados: não observe um elemento dentro do callback de outro observador sem controle. Isso pode criar loops infinitos.
Gerenciar ciclo de vida: sempre chame unobserve() ou disconnect() quando o elemento for removido do DOM ou não for mais necessário.
function cleanup(observer, element) {
if (observer && element) {
observer.unobserve(element);
}
}
Throttle/Debounce no callback: mesmo que os observadores sejam otimizados, o callback pode disparar muitas vezes em animações ou redimensionamentos contínuos. Use requestAnimationFrame para agrupar alterações:
const resizeObserver = new ResizeObserver(entries => {
window.requestAnimationFrame(() => {
for (const entry of entries) {
// Processa as mudanças
}
});
});
6. Tratamento de Casos Extremos e Fallbacks
Elementos ocultos: ResizeObserver não dispara para elementos com display: none. Se precisar monitorar elementos que alternam visibilidade, use MutationObserver combinado ou force um reflow ao torná-los visíveis.
Navegadores antigos: para suporte a IE11 e versões antigas, utilize polyfills:
Intersecção com iframes: o IntersectionObserver funciona entre iframes apenas se ambos os documentos estiverem na mesma origem. Caso contrário, utilize postMessage para comunicação.
Rolagem suave: scroll-behavior: smooth pode atrasar a detecção de intersecção. Ajuste o rootMargin para compensar.
7. Integração com Frameworks e Bibliotecas Modernas
React — hook customizado useResizeObserver:
import { useEffect, useRef, useState } from 'react';
function useResizeObserver(ref) {
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
const observer = new ResizeObserver(entries => {
if (entries[0]) {
const { width, height } = entries[0].contentRect;
setDimensions({ width, height });
}
});
if (ref.current) {
observer.observe(ref.current);
}
return () => observer.disconnect();
}, [ref]);
return dimensions;
}
Vue — composable useIntersectionObserver:
import { ref, onMounted, onUnmounted } from 'vue';
export function useIntersectionObserver(targetRef, options = {}) {
const isVisible = ref(false);
let observer = null;
onMounted(() => {
observer = new IntersectionObserver(([entry]) => {
isVisible.value = entry.isIntersecting;
}, options);
if (targetRef.value) {
observer.observe(targetRef.value);
}
});
onUnmounted(() => {
if (observer) observer.disconnect();
});
return { isVisible };
}
8. Conclusão e Próximos Passos
Os observadores nativos ResizeObserver e IntersectionObserver transformaram a forma como construímos experiências responsivas. Eles oferecem:
- Performance: sem polling ou listeners pesados
- Adaptabilidade: layouts que respondem ao espaço disponível
- Economia de recursos: carregamento sob demanda e pausa de mídia
Para implementar em projetos reais, siga este checklist:
- [ ] Identifique elementos que precisam de redimensionamento dinâmico
- [ ] Substitua event listeners de scroll/resize por observadores
- [ ] Configure thresholds e rootMargin adequados
- [ ] Implemente fallbacks para navegadores legados
- [ ] Teste em dispositivos móveis e diferentes viewports
Esses observadores são fundamentais para criar interfaces que se adaptam ao usuário, não o contrário.
Referências
- MDN Web Docs: ResizeObserver — Documentação oficial completa com exemplos e especificações técnicas
- MDN Web Docs: IntersectionObserver — Guia detalhado da API, incluindo todos os parâmetros de configuração
- Google Web Dev: IntersectionObserver - Comece a usar — Tutorial prático com casos de uso para performance e UX
- CSS-Tricks: Como usar ResizeObserver — Artigo técnico com exemplos avançados de aplicação
- ResizeObserver Polyfill no GitHub — Polyfill oficial para suporte em navegadores antigos
- IntersectionObserver Polyfill no W3C — Polyfill mantido pelo W3C para compatibilidade estendida