Eventos y tiempos · 2 min de lectura

¿Debounce o throttle?

Ambos limitan cuántas veces se ejecuta un handler. Elige según lo que el usuario deba ver mientras siguen llegando eventos.

Algunos eventos se disparan mucho más a menudo de lo necesario: input con cada tecla, scroll y pointermove decenas de veces por segundo. Debounce y throttle reducen el número de llamadas, pero se comportan de forma distinta mientras los eventos siguen llegando.

Dos rachas con 56 eventos en total, como al escribir o desplazarse. Debounce se ejecuta 2 veces, 200 ms después del último evento de cada racha; throttle 9 veces: enseguida, luego como mucho cada 100 ms y una vez más al final; requestAnimationFrame 33 veces, como mucho una por fotograma. Los instantes están calculados con exactamente las funciones de abajo.eventollamadaesperallamada por fotograma

Debounce: esperar a que haya calma

Una función con debounce solo se ejecuta cuando los eventos se han detenido durante cierto tiempo. Encaja con trabajo que solo tiene sentido para el valor final, como enviar una búsqueda o guardar un borrador.

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

input.addEventListener('input', debounce(search, 250));

Throttle: seguir, pero más despacio

Una función con throttle se ejecuta como mucho una vez por intervalo mientras los eventos continúan. Úsala cuando el usuario deba ver algún cambio durante la interacción, por ejemplo una indicación de posición mientras arrastra.

function throttle(fn, wait) {
  let last = 0;
  let timer;
  return function (...args) {
    const remaining = wait - (Date.now() - last);
    clearTimeout(timer);
    if (remaining <= 0) {
      last = Date.now();
      fn.apply(this, args);
    } else {
      timer = setTimeout(() => {
        last = Date.now();
        fn.apply(this, args);
      }, remaining);
    }
  };
}

Esta versión llama a la función de inmediato, después como mucho cada wait milisegundos y una vez más al final, para que el último valor nunca se pierda.

Para todo lo visual, la frecuencia de fotogramas

Si el handler solo actualiza lo que se ve en pantalla, el intervalo natural es un fotograma. requestAnimationFrame te da exactamente eso, sin tener que elegir un número:

let queued = false;
window.addEventListener('scroll', () => {
  if (queued) return;
  queued = true;
  requestAnimationFrame(() => {
    queued = false;
    updateProgressBar(window.scrollY);
  });
}, { passive: true });

Como regla rápida: debounce para el resultado, throttle para la respuesta visual y requestAnimationFrame para los píxeles. Por qué rAF se ejecuta siempre justo antes de la siguiente imagen lo cuenta Tareas, microtareas y requestAnimationFrame: quién va primero.

A veces no hace falta JavaScript

La barra de progreso de la parte superior de esta página sigue cada desplazamiento y, sin embargo, no se ejecuta ni un solo handler. Los navegadores con animaciones vinculadas al scroll enlazan una animación CSS directamente con la posición de desplazamiento:

.progress {
  position: fixed;
  inset: 0 0 auto;
  height: 3px;
  transform-origin: left;
  animation: grow linear both;
  animation-timeline: scroll(root);
}
@keyframes grow {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

Como solo se anima transform, el compositor puede mover la barra incluso cuando el hilo principal está ocupado. Los navegadores que aún no lo admiten simplemente no muestran la barra.

← Todas las notas