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
Nmilisegundos 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
Nmilisegundos, sin importar cuántas veces se llame en ese intervalo. Piensa: "latido regular."
| Aspecto | Debounce | Throttle |
|---|---|---|
| Se dispara cuando | La actividad se detiene durante N ms | Como máximo una vez cada N ms |
| Durante una ráfaga | Nada se ejecuta hasta que la ráfaga termina | Se ejecuta en un horario fijo |
| Modelo mental | "Esperar el silencio" | "Cadencia constante" |
| Útil para | Búsqueda mientras escribes, autoguardado, resize terminado | Seguimiento 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:
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.
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
debounceanterior 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
throttleanterior 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.
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.
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.