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 callback
  • rootMargin: 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