Back

Manejo de Eventos de Scroll Sin Afectar el Rendimiento

Manejo de Eventos de Scroll Sin Afectar el Rendimiento

Los eventos de scroll se disparan decenas de veces por segundo. Sin un manejo adecuado, arruinarán el rendimiento de tu sitio, agotarán las baterías de dispositivos móviles y crearán experiencias de usuario entrecortadas. Aquí te mostramos cómo optimizar los manejadores de scroll usando throttling, debouncing y listeners pasivos.

Puntos Clave

  • El throttling limita la ejecución de funciones a intervalos fijos para actualizaciones consistentes
  • El debouncing espera hasta que el scroll se detenga antes de ejecutar operaciones costosas
  • Los listeners pasivos permiten optimizaciones inmediatas del navegador
  • Intersection Observer elimina los eventos de scroll para detección de visibilidad

El Problema: Por Qué los Eventos de Scroll Sin Procesar Destruyen el Rendimiento

Cada movimiento de scroll dispara múltiples eventos—frecuentemente más de 60 por segundo. Cuando adjuntas cálculos pesados a estos eventos, le estás pidiendo al navegador que:

  • Bloquee el hilo principal repetidamente
  • Impida el scroll suave
  • Aumente dramáticamente el uso de CPU
  • Agote la batería en dispositivos móviles
// NO HAGAS ESTO - se dispara constantemente
window.addEventListener('scroll', () => {
  calculateExpensiveAnimation();
  updateNavigationState();
  checkElementVisibility();
});

Solución 1: Throttle para Actualizaciones Consistentes

El throttling limita la ejecución de funciones a un intervalo fijo. Perfecto para animaciones basadas en scroll o indicadores de progreso que necesitan actualizaciones 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);
    }
  };
}

// Se dispara como máximo cada 100ms
window.addEventListener('scroll', throttle(() => {
  updateScrollProgress();
}, 100));

Cuándo usar throttling:

  • Barras de progreso de scroll
  • Efectos parallax
  • Actualizaciones de estado de navegación
  • Seguimiento de posición en tiempo real

Solución 2: Debounce para Valores Finales

El debouncing espera hasta que el scroll se detenga antes de ejecutar. Ideal para operaciones costosas que solo necesitan la posición final del scroll.

function debounce(func, wait) {
  let timeout;
  return function(...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, args), wait);
  };
}

// Se dispara 200ms después de que el scroll se detenga
window.addEventListener('scroll', debounce(() => {
  saveScrollPosition();
  loadMoreContent();
}, 200));

Cuándo usar debouncing:

  • Disparadores de scroll infinito
  • Seguimiento de analytics
  • Guardar posición de scroll
  • Cálculos pesados del DOM

Solución 3: Listeners Pasivos para Rendimiento Instantáneo

Los listeners pasivos le dicen al navegador que no llamarás preventDefault(), habilitando optimizaciones inmediatas de scroll.

// El navegador puede optimizar el scroll inmediatamente
window.addEventListener('scroll', handleScroll, { passive: true });

Esta simple bandera mejora el rendimiento del scroll al permitir que el navegador omita verificar si vas a prevenir el comportamiento predeterminado. Los navegadores móviles se benefician especialmente de esta optimización.

Combinando Técnicas para Máximo Rendimiento

Para interacciones complejas de scroll, combina múltiples enfoques:

// Manejador con throttle y listener pasivo
const optimizedScroll = throttle(() => {
  requestAnimationFrame(() => {
    updateUI();
  });
}, 16); // ~60fps

window.addEventListener('scroll', optimizedScroll, { passive: true });

Alternativa Moderna: Intersection Observer

Para detección de visibilidad, omite completamente los eventos de scroll:

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      lazyLoadImage(entry.target);
    }
  });
});

observer.observe(document.querySelector('.lazy-image'));

Intersection Observer maneja la detección de visibilidad sin ningún listener de scroll, ofreciendo rendimiento superior para lazy loading y animaciones disparadas por scroll.

Guía de Decisión Rápida

TécnicaCaso de UsoFrecuencia de Actualización
ThrottleBarras de progreso, parallaxIntervalos fijos
DebounceGuardar estado, cargar contenidoDespués de que el scroll se detenga
PasivoCualquier manejador de scrollSiempre (cuando sea posible)
Intersection ObserverDetección de visibilidadSin eventos de scroll

Consejos de Implementación

  1. Siempre usa listeners pasivos a menos que necesites preventDefault()
  2. Comienza con delays de throttle de 16ms para animaciones a 60fps
  3. Usa delays de debounce de 200-300ms para acciones disparadas por el usuario
  4. Considera Lodash para implementaciones probadas en batalla
  5. Perfila con Chrome DevTools para medir las ganancias reales de rendimiento

Conclusión

Los manejadores de scroll no optimizados son asesinos del rendimiento. El throttling te da actualizaciones controladas para animaciones, el debouncing maneja valores finales eficientemente, y los listeners pasivos proporcionan optimizaciones instantáneas del navegador. Para detección de visibilidad, omite completamente los eventos de scroll con Intersection Observer. Elige la técnica correcta para tu caso de uso, y tus usuarios te lo agradecerán con la vida de su batería.

Preguntas Frecuentes

El throttling ejecuta tu función a intervalos regulares durante el scroll, como cada 100ms. El debouncing espera hasta que el scroll se detenga completamente, luego ejecuta una vez. Usa throttling para actualizaciones continuas y debouncing para valores finales.

No, los listeners pasivos explícitamente le dicen al navegador que no llamarás preventDefault. Si necesitas prevenir el comportamiento predeterminado del scroll, establece passive en false, pero esto impacta el rendimiento. Considera enfoques alternativos primero.

Usa Intersection Observer para cualquier lógica basada en visibilidad como lazy loading, disparadores de scroll infinito, o animaciones en scroll. Es más eficiente que los eventos de scroll y maneja automáticamente los cálculos de viewport sin verificación manual de posición.

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.

OpenReplay