W3docs

Debounce y Throttle en JavaScript

Aprende a limitar la frecuencia de funciones en JavaScript con debounce y throttle: qué hace cada uno, cómo implementarlos y cuándo usar cada uno.

Algunos eventos se disparan con mucha más frecuencia de la que puedes responder de forma útil. Escribir en un cuadro de búsqueda genera un evento input en cada pulsación de tecla; desplazarse por una página puede emitir cientos de eventos scroll por segundo; resize y mousemove son igual de locuaces. Si cada evento ejecuta trabajo costoso — una petición de red, un cálculo de diseño, un re-renderizado — tu aplicación se bloquea. Debounce y throttle son dos pequeños envoltorios que limitan la frecuencia de ejecución de una función, manteniendo alta la capacidad de respuesta sin cambiar lo que hace la función.

Ambos son patrones clásicos de decorador: reciben una función y devuelven una nueva función con el mismo comportamiento más una regla de limitación de frecuencia. Se construyen sobre clausuras para recordar el estado entre llamadas y sobre temporizadores como setTimeout para diferir o controlar la ejecución.

La idea central

Las dos técnicas responden a la misma pregunta — "¿con qué frecuencia debería ejecutarse esto?" — de maneras opuestas:

  • Debounce espera una pausa. Pospone la llamada hasta que hayan pasado N milisegundos desde la última invocación. Si las llamadas siguen llegando, el temporizador se reinicia y la función nunca se ejecuta. Piensa: "esperar el silencio."
  • Throttle aplica un ritmo constante. Permite que la función se ejecute como máximo una vez cada N milisegundos, sin importar cuántas veces se llame en ese intervalo. Piensa: "latido regular."
AspectoDebounceThrottle
Se dispara cuandoLa actividad se detiene durante N msComo máximo una vez cada N ms
Durante una ráfagaNada se ejecuta hasta que la ráfaga terminaSe ejecuta en un horario fijo
Modelo mental"Esperar el silencio""Cadencia constante"
Útil paraBúsqueda mientras escribes, autoguardado, resize terminadoSeguimiento de scroll, arrastrar, scroll infinito

Debounce

Una función con debounce cancela cualquier temporizador pendiente en cada llamada y programa uno nuevo. Solo cuando las llamadas finalmente se detienen durante delay milisegundos se ejecuta realmente la función envuelta.

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

Dos detalles hacen que esto sea robusto. El envoltorio recopila todos los argumentos con parámetros rest (...args) y los reenvía, de modo que la función envuelta recibe exactamente lo que pasó el invocador. Y llama a fn con fn.apply(this, args) para que el this original se preserve — algo importante cuando la función con debounce es un método de un objeto. (Consulta call y apply y enlace de funciones para entender por qué importa reenviar this.)

Aquí está en acción. Llamar a la función envuelta repetidamente solo genera una ejecución real, después de que la actividad se estabiliza:

javascript— editable

Debido a que cada pulsación de tecla reinicia el reloj, debounce es ideal siempre que quieras reaccionar después de que el usuario termine: búsqueda mientras escribe, autoguardado de un borrador, validación de un campo una vez que se deja de escribir, o recalcular un diseño solo cuando el cambio de tamaño de una ventana se ha estabilizado.

Throttle

Una función con throttle se ejecuta de inmediato, luego ignora llamadas adicionales hasta que transcurre un tiempo de espera. Esto garantiza una frecuencia máxima en lugar de esperar una pausa.

function throttle(fn, limit) {
  let inThrottle = false;
  return function (...args) {
    if (!inThrottle) {
      fn.apply(this, args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}

El indicador inThrottle, mantenido en la clausura, actúa como una compuerta. La primera llamada pasa y la compuerta se cierra; las llamadas durante el tiempo de espera se descartan; cuando el temporizador se dispara, la compuerta se reabre para la siguiente llamada.

javascript— editable

Throttle es adecuado para cualquier cosa que fluya continuamente y donde quieras actualizaciones regulares en lugar de cada una: seguimiento de la posición del scroll, manejo de mousemove durante un arrastre, cargar más contenido en scroll infinito, o limitar la frecuencia con la que accedes a una API con límite de solicitudes.

Borde inicial vs. borde final

Hay una elección de diseño sutil en ambos envoltorios: ¿debería la función dispararse en el borde inicial (la primera llamada, de inmediato) o en el borde final (después del retraso/tiempo de espera)?

  • El debounce anterior es de borde final: nada sucede hasta que la actividad se detiene. Un debounce de borde inicial se ejecutaría en la primera llamada y luego ignoraría el resto.
  • El throttle anterior es de borde inicial: se dispara de inmediato y luego controla. Un throttle de borde final también se ejecutaría una vez más al final de la ventana para capturar el valor final.

Estos comportamientos de borde importan en la práctica — un throttle de borde final en el scroll, por ejemplo, garantiza que no se pierda la posición final del scroll cuando el usuario se detiene.

Información

Para código en producción, prefiere una implementación probada en batalla como _.debounce y _.throttle de lodash. Manejan los bordes inicial y final, una API cancel()/flush(), y una opción maxWait (para que una función con debounce se ejecute eventualmente durante actividad continua). Entender las versiones básicas anteriores es esencial, pero rara vez necesitarás enviar la tuya propia.

Un ejemplo real con el DOM

Conectar debounce a un campo de búsqueda es el caso de uso canónico. Adjuntamos un oyente (consulta manejo de eventos en el DOM) y dejamos que el envoltorio decida cuándo se ejecuta realmente el trabajo:

const input = document.querySelector('#search');

function search(event) {
  console.log('Querying API for:', event.target.value);
  // fetch(`/api/search?q=${event.target.value}`) ...
}

const debouncedSearch = debounce(search, 400);

input.addEventListener('input', debouncedSearch);

Ahora la petición de red solo se dispara cuando el usuario hace una pausa de 400 ms, en lugar de en cada pulsación de tecla — un cuadro de búsqueda que antes disparaba una docena de peticiones para hello ahora dispara una. Ten en cuenta que el oyente recibe el objeto event del DOM y, como nuestro envoltorio reenvía todos los argumentos, search aún lo recibe intacto.

Advertencia

Los temporizadores y los oyentes mantienen referencias, por lo que debes limpiarlos cuando ya no sean necesarios. En una aplicación de una sola página o componente, elimina el oyente al desmontarse (por ejemplo, al desmontar el componente) y cancela cualquier temporizador pendiente para evitar fugas de memoria y que los callbacks se disparen en elementos que ya no existen:

input.removeEventListener('input', debouncedSearch);

Un debounce de producción generalmente también expone un método cancel() que llama a clearTimeout por ti.

Cómo elegir entre ellos

Cuando no estés seguro de cuál usar, pregúntate qué te importa:

  • ¿Solo te importa el estado final después de una ráfaga de actividad (el término de búsqueda terminado, el tamaño de ventana estabilizado)? Usa debounce.
  • ¿Quieres retroalimentación continua y regular durante la actividad (progreso del scroll, posición de un elemento arrastrado)? Usa throttle.

Ambos son ligeros, independientes del framework, y se construyen directamente sobre clausuras y temporizadores — los mismos fundamentos detrás de las arrow functions que capturan this y las herramientas de programación que ya has visto.

Pon a prueba tus conocimientos

Práctica
¿Qué técnica ejecuta la función como máximo una vez por intervalo de tiempo fijo, sin importar con qué frecuencia se llame?
¿Qué técnica ejecuta la función como máximo una vez por intervalo de tiempo fijo, sin importar con qué frecuencia se llame?
Práctica
Quieres enviar una petición de búsqueda solo después de que el usuario deje de escribir. ¿Cuál es la herramienta correcta?
Quieres enviar una petición de búsqueda solo después de que el usuario deje de escribir. ¿Cuál es la herramienta correcta?
Práctica
¿Por qué los ejemplos de debounce y throttle llaman a la función original con `fn.apply(this, args)` en lugar de simplemente `fn(args)`?
¿Por qué los ejemplos de debounce y throttle llaman a la función original con `fn.apply(this, args)` en lugar de simplemente `fn(args)`?
Was this page helpful?