Gerenciando Eventos de Scroll Sem Prejudicar a Performance

Eventos de scroll são disparados dezenas de vezes por segundo. Sem o tratamento adequado, eles irão destruir a performance do seu site, drenar baterias de dispositivos móveis e criar experiências de usuário travadas. Veja como otimizar handlers de scroll usando throttling, debouncing e passive listeners.
Principais Pontos
- Throttling limita a execução de funções a intervalos fixos para atualizações consistentes
- Debouncing aguarda até que o scroll pare antes de executar operações custosas
- Passive listeners habilitam otimizações imediatas do navegador
- Intersection Observer elimina eventos de scroll para detecção de visibilidade
O Problema: Por Que Eventos de Scroll Brutos Destroem a Performance
Cada movimento de scroll dispara múltiplos eventos—frequentemente 60+ por segundo. Quando você anexa computações pesadas a esses eventos, você está pedindo para o navegador:
- Bloquear a thread principal repetidamente
- Impedir scroll suave
- Aumentar drasticamente o uso de CPU
- Drenar a bateria em dispositivos móveis
// NÃO FAÇA ISSO - dispara constantemente
window.addEventListener('scroll', () => {
calculateExpensiveAnimation();
updateNavigationState();
checkElementVisibility();
});
Solução 1: Throttle para Atualizações Consistentes
Throttling limita a execução de funções a um intervalo fixo. Perfeito para animações baseadas em scroll ou indicadores de progresso que precisam de atualizações regulares.
function throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
return func.apply(this, args);
}
};
}
// Dispara no máximo a cada 100ms
window.addEventListener('scroll', throttle(() => {
updateScrollProgress();
}, 100));
Quando usar throttling:
- Barras de progresso de scroll
- Efeitos parallax
- Atualizações de estado de navegação
- Rastreamento de posição em tempo real
Solução 2: Debounce para Valores Finais
Debouncing aguarda até que o scroll pare antes de executar. Ideal para operações custosas que precisam apenas da posição final do scroll.
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// Dispara 200ms após o scroll parar
window.addEventListener('scroll', debounce(() => {
saveScrollPosition();
loadMoreContent();
}, 200));
Quando usar debouncing:
- Triggers de scroll infinito
- Rastreamento de analytics
- Salvamento de posição de scroll
- Cálculos pesados do DOM
Discover how at OpenReplay.com.
Solução 3: Passive Listeners para Performance Instantânea
Passive listeners informam ao navegador que você não chamará preventDefault()
, habilitando otimizações imediatas de scroll.
// O navegador pode otimizar o scroll imediatamente
window.addEventListener('scroll', handleScroll, { passive: true });
Esta simples flag melhora a performance do scroll permitindo que o navegador pule a verificação se você vai prevenir o comportamento padrão. Navegadores móveis se beneficiam especialmente desta otimização.
Combinando Técnicas para Performance Máxima
Para interações complexas de scroll, combine múltiplas abordagens:
// Handler com throttle e passive listener
const optimizedScroll = throttle(() => {
requestAnimationFrame(() => {
updateUI();
});
}, 16); // ~60fps
window.addEventListener('scroll', optimizedScroll, { passive: true });
Alternativa Moderna: Intersection Observer
Para detecção de visibilidade, pule eventos de scroll completamente:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
lazyLoadImage(entry.target);
}
});
});
observer.observe(document.querySelector('.lazy-image'));
Intersection Observer gerencia detecção de visibilidade sem nenhum listener de scroll, oferecendo performance superior para lazy loading e animações disparadas por scroll.
Guia de Decisão Rápida
Técnica | Caso de Uso | Frequência de Atualização |
---|---|---|
Throttle | Barras de progresso, parallax | Intervalos fixos |
Debounce | Salvar estado, carregar conteúdo | Após o scroll parar |
Passive | Qualquer handler de scroll | Sempre (quando possível) |
Intersection Observer | Detecção de visibilidade | Sem eventos de scroll |
Dicas de Implementação
- Sempre use passive listeners a menos que você precise de
preventDefault()
- Comece com delays de throttle de 16ms para animações a 60fps
- Use delays de debounce de 200-300ms para ações disparadas pelo usuário
- Considere Lodash para implementações testadas em batalha
- Profile com Chrome DevTools para medir ganhos reais de performance
Conclusão
Handlers de scroll não otimizados são assassinos de performance. Throttling oferece atualizações controladas para animações, debouncing gerencia valores finais eficientemente, e passive listeners fornecem otimizações instantâneas do navegador. Para detecção de visibilidade, pule eventos de scroll completamente com Intersection Observer. Escolha a técnica certa para seu caso de uso, e seus usuários agradecerão com a vida útil da bateria.
FAQs
Throttling executa sua função em intervalos regulares durante o scroll, como a cada 100ms. Debouncing aguarda até que o scroll pare completamente, então executa uma vez. Use throttling para atualizações contínuas e debouncing para valores finais.
Não, passive listeners explicitamente informam ao navegador que você não chamará preventDefault. Se você precisa prevenir o comportamento padrão de scroll, defina passive como false, mas isso impacta a performance. Considere abordagens alternativas primeiro.
Use Intersection Observer para qualquer lógica baseada em visibilidade como lazy loading, triggers de scroll infinito, ou animações no scroll. É mais performático que eventos de scroll e automaticamente gerencia cálculos de viewport sem verificação manual de posição.
Gain control over your UX
See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay. — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.