Debounce y throttle: controlar la frecuencia de eventos
Algunos eventos del navegador se disparan cientos de veces por segundo. Debounce y throttle son las herramientas para controlar esa frecuencia y proteger el rendimiento de tu app.
El problema — eventos que se disparan sin control
Eventos como scroll, resize, mousemove y input pueden dispararse cientos de veces por segundo. Llamar a una función costosa en cada disparo degrada el rendimiento severamente.
// Simulación del problema — sin control
let contadorScroll = 0;
let contadorInput = 0;
// Esto se dispara potencialmente 60 veces/segundo al hacer scroll
document.addEventListener("scroll", () => {
contadorScroll++;
// ❌ Mal: una llamada a la API por cada pixel de scroll
// fetchSugerencias(); // ← costaría miles de llamadas
console.log(`scroll event #${contadorScroll}`);
});
// Esto se dispara por cada tecla presionada
document.querySelector("#busqueda")?.addEventListener("input", (e) => {
contadorInput++;
// ❌ Mal: una petición HTTP por cada carácter
// buscarEnAPI(e.target.value); // ← inunda el servidor
});
// Con debounce y throttle, controlamos cuándo se ejecuta la función
// sin modificar el listener del evento
// Consola de Node.js — simular eventos
function simularEventos(fn, veces = 10) {
for (let i = 0; i < veces; i++) {
fn(i);
}
}
simularEventos(i => console.log(`Evento ${i}`));// El problema en TypeScript — tipos de evento
type Handler<T = void> = (evento?: T) => void;
function simularEventosRapidos<T>(
handler: Handler<T>,
veces: number = 10,
intervaloMs: number = 50
): void {
for (let i = 0; i < veces; i++) {
setTimeout(() => handler(), i * intervaloMs);
}
}
// Sin control, 10 llamadas en 500ms
let contador = 0;
simularEventosRapidos(() => {
contador++;
console.log(`Llamada #${contador}`);
}, 10, 50);
// Con debounce o throttle, podríamos reducirlas a 1-3Evento 0
Evento 1
...Evento 9
Llamada #1
...Llamada #10Debounce — ejecutar solo después de que deja de dispararse
Debounce retrasa la ejecución de la función hasta que hayan pasado N milisegundos sin que el evento se vuelva a disparar.
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
// Cancelar el timer anterior
clearTimeout(timeoutId);
// Iniciar un nuevo timer
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// Uso — búsqueda con debounce
function buscarEnAPI(termino) {
console.log(`Buscando: "${termino}"`);
}
const buscarConDebounce = debounce(buscarEnAPI, 300);
// Simular el usuario escribiendo "hola"
buscarConDebounce("h"); // cancela
buscarConDebounce("ho"); // cancela
buscarConDebounce("hol"); // cancela
buscarConDebounce("hola"); // espera 300ms y ejecuta
// Solo una llamada a la API después de 300ms de silencio
// Output: "Buscando: "hola""
// Debounce con inmediato — ejecuta al primer disparo, luego espera
function debounceInmediato(fn, delay) {
let timeoutId;
return function(...args) {
const ejecutarAhora = !timeoutId;
clearTimeout(timeoutId);
timeoutId = setTimeout(() => { timeoutId = null; }, delay);
if (ejecutarAhora) fn.apply(this, args);
};
}function debounce<T extends unknown[]>(
fn: (...args: T) => void,
delay: number
): (...args: T) => void {
let timeoutId: ReturnType<typeof setTimeout> | undefined;
return function(this: unknown, ...args: T): void {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// Uso tipado
function buscarUsuarios(termino: string): void {
console.log(`Buscando usuarios: "${termino}"`);
// fetch(`/api/usuarios?q=${termino}`)
}
const buscarConDebounce = debounce(buscarUsuarios, 300);
// Simular escritura
buscarConDebounce("c");
buscarConDebounce("ca");
buscarConDebounce("car");
buscarConDebounce("carlos");
// Solo ejecuta con "carlos" después de 300ms de silencio
// Debounce para validación de formulario
const validarEmailDebounced = debounce((email: string) => {
const valido = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
console.log(`Email "${email}" es ${valido ? "válido" : "inválido"}`);
}, 500);Buscando: "hola"
Buscando usuarios: "carlos"Throttle — ejecutar máximo una vez cada N ms
Throttle garantiza que la función se ejecute como máximo una vez cada N milisegundos, sin importar cuántas veces se dispare el evento.
function throttle(fn, limite) {
let ultimaEjecucion = 0;
return function(...args) {
const ahora = Date.now();
if (ahora - ultimaEjecucion >= limite) {
ultimaEjecucion = ahora;
return fn.apply(this, args);
}
};
}
// Uso — posición del scroll
function actualizarBarraProgreso(posicion) {
const porcentaje = Math.round((posicion / document.body.scrollHeight) * 100);
console.log(`Progreso: ${porcentaje}%`);
}
const actualizarConThrottle = throttle(actualizarBarraProgreso, 200);
// Aunque scroll dispare 60 veces/segundo,
// actualizarBarraProgreso solo se llama 5 veces/segundo (cada 200ms)
// Simular 10 llamadas rápidas
let simulado = 0;
const fn = throttle(() => {
simulado++;
console.log(`Ejecutado: #${simulado} a las ${Date.now()}`);
}, 500);
fn(); // ejecuta — t=0
fn(); // ignora — t=0
fn(); // ignora — t=0
// ...después de 500ms...
// fn() → ejecutafunction throttle<T extends unknown[]>(
fn: (...args: T) => void,
limite: number
): (...args: T) => void {
let ultimaEjecucion: number = 0;
return function(this: unknown, ...args: T): void {
const ahora = Date.now();
if (ahora - ultimaEjecucion >= limite) {
ultimaEjecucion = ahora;
fn.apply(this, args);
}
};
}
// Throttle para posición del mouse
interface Posicion {
x: number;
y: number;
}
function registrarPosicion(pos: Posicion): void {
console.log(`Mouse en: (${pos.x}, ${pos.y})`);
}
const registrarConThrottle = throttle(registrarPosicion, 100);
// Simular movimiento del mouse — solo registra cada 100ms
document.addEventListener("mousemove", (e: MouseEvent) => {
registrarConThrottle({ x: e.clientX, y: e.clientY });
});
// Throttle para infinite scroll
function cargarMasDatos(): void {
console.log("Cargando más items...");
}
const cargarConThrottle = throttle(cargarMasDatos, 1000);
// Aunque el scroll llegue al fondo muchas veces, solo carga una vez por segundoEjecutado: #1 a las ...
Mouse en: (245, 380)
Cargando más items...Cuándo usar cada uno
| Aspecto | Debounce | Throttle |
|---|---|---|
| Comportamiento | Espera el silencio antes de ejecutar | Ejecuta una vez cada N ms como máximo |
| Caso de uso típico | Search input, validación de formulario | Scroll, resize, mousemove, rate limiting |
| ¿Se ejecuta mientras el evento activo? | No — solo al final, cuando hay silencio | Sí — a intervalos regulares |
| Garantía de ejecución | Se ejecuta después de que para el evento | Puede perderse el último disparo |
| Latencia de respuesta | Igual al delay configurado | Inmediata en el primer disparo |
Si el usuario espera el resultado de su última acción (escribir en un buscador), usa debounce. Si quieres actualización continua pero controlada (barra de progreso de scroll), usa throttle.