CAP 14 · LEC 04·Bonus prácticos

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.

● INTERMEDIO8 min lectura3 ejerciciospor Fernando Herrera · actualizado mayo de 2026
¿Encontraste un error o algo que mejorar?Editá esta lección en GitHub →

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-3
SalidaEvento 0 Evento 1 ...Evento 9 Llamada #1 ...Llamada #10

Debounce — 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);
SalidaBuscando: "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() → ejecuta
function 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 segundo
SalidaEjecutado: #1 a las ... Mouse en: (245, 380) Cargando más items...

Cuándo usar cada uno

AspectoDebounceThrottle
ComportamientoEspera el silencio antes de ejecutarEjecuta una vez cada N ms como máximo
Caso de uso típicoSearch input, validación de formularioScroll, resize, mousemove, rate limiting
¿Se ejecuta mientras el evento activo?No — solo al final, cuando hay silencioSí — a intervalos regulares
Garantía de ejecuciónSe ejecuta después de que para el eventoPuede perderse el último disparo
Latencia de respuestaIgual al delay configuradoInmediata en el primer disparo
Regla práctica

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.

Practica