Debounce y throttle son dos técnicas para controlar la frecuencia con la que se ejecuta una función en respuesta a eventos de alta frecuencia, como el scroll, el resize o la escritura en un input. Sin ellas, un handler de scroll puede ejecutarse cientos de veces por segundo, degradando el rendimiento de la aplicación.
Debounce: esperar a que el usuario pare
Debounce retrasa la ejecución hasta que han pasado N milisegundos desde la última llamada. Si la función se invoca de nuevo antes de que expire el temporizador, este se reinicia:
function debounce(fn, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer); // Cancelar el timer anterior
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// Caso de uso: búsqueda en tiempo real
const input = document.querySelector('#busqueda');
const buscarDebounced = debounce(async (termino) => {
if (termino.length < 2) return;
const resultados = await fetch(`/api/buscar?q=${termino}`).then(r => r.json());
mostrarResultados(resultados);
}, 300); // Espera 300ms desde el último keystroke
input.addEventListener('input', (e) => buscarDebounced(e.target.value));
// Sin debounce: con escritura rápida de "javascript"
// Se harían 10 peticiones al servidor
// Con debounce 300ms: solo 1 petición (cuando el usuario para)
Implementación completa de debounce con opción immediate
La variante "leading edge" ejecuta la función inmediatamente en la primera llamada y luego espera a que expire el delay antes de permitir otra ejecución:
function debounceCompleto(fn, delay, opciones = {}) {
const { immediato = false } = opciones;
let timer = null;
return function(...args) {
const contexto = this;
const ejecutarAhora = immediato && !timer;
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
if (!immediato) fn.apply(contexto, args);
}, delay);
if (ejecutarAhora) fn.apply(contexto, args);
};
}
// Debounce immediato: ejecuta en el PRIMER clic, luego ignora los siguientes
// hasta que pasen los 500ms
const boton = document.querySelector('#enviar');
boton.addEventListener('click', debounceCompleto(() => {
enviarFormulario();
}, 500, { immediato: true }));
Throttle: limitar a una ejecución cada N ms
Throttle garantiza que la función no se ejecuta más de una vez en cada ventana de tiempo N. A diferencia de debounce, no espera a que el usuario pare: ejecuta regularmente durante 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);
}
};
}
// Caso de uso: guardar la posición de scroll
const guardarPosicion = throttle(() => {
localStorage.setItem('scrollY', window.scrollY.toString());
}, 500); // Máximo 2 veces por segundo
window.addEventListener('scroll', guardarPosicion);
// Caso de uso: actualizar una barra de progreso al mover el ratón
const actualizarProgreso = throttle((e) => {
const progreso = (e.clientX / window.innerWidth * 100).toFixed(1);
document.querySelector('.progreso').style.width = `${progreso}%`;
}, 16); // ~60fps
document.addEventListener('mousemove', actualizarProgreso);
Throttle con requestAnimationFrame
Para actualizaciones visuales, es mejor sincronizar con el ciclo de renderizado del navegador en lugar de usar un timer fijo:
function throttleRAF(fn) {
let frameId = null;
return function(...args) {
if (frameId) return; // Ya hay una ejecución pendiente
frameId = requestAnimationFrame(() => {
fn.apply(this, args);
frameId = null;
});
};
}
// Para animaciones de scroll: perfectamente sincronizado con el monitor
const manejarScroll = throttleRAF(() => {
const cabecera = document.querySelector('header');
if (window.scrollY > 100) {
cabecera.classList.add('fija');
} else {
cabecera.classList.remove('fija');
}
});
window.addEventListener('scroll', manejarScroll);
Debounce vs throttle: cuándo usar cada uno
// USA DEBOUNCE cuando:
// - Búsqueda en tiempo real (esperar a que el usuario deje de escribir)
// - Guardado automático en formulario
// - Validación de campo al salir del input
// - Redimensionar ventana (calcular layout solo cuando termina)
const validarEmail = debounce((email) => {
const valido = /^[w.]+@w+.w+$/.test(email);
mostrarEstado(valido ? 'ok' : 'error');
}, 400);
// USA THROTTLE cuando:
// - Scroll (actualizar navbar, lazy loading)
// - Mousemove (tooltips, seguimiento de cursor)
// - Resize (actualizar gráficos periódicamente durante el resize)
// - Juegos (límite de disparos por segundo)
const actualizarNavbar = throttle(() => {
const esFija = window.scrollY > 50;
document.body.classList.toggle('navbar-fija', esFija);
}, 100);
El error más habitual al usar debounce
// MAL: se crea una nueva función debounced en cada render
// Cada instancia tiene su propio timer, por lo que el debounce no funciona
function Componente() {
const buscar = debounce(hacerBusqueda, 300); // Nueva instancia en cada render
return <input onChange={e => buscar(e.target.value)} />;
}
// BIEN: en React, usar useCallback o useMemo para preservar la referencia
function Componente() {
const buscar = useCallback(
debounce(hacerBusqueda, 300),
[] // Sin dependencias: se crea solo una vez
);
return <input onChange={e => buscar(e.target.value)} />;
}
En la práctica, la mayoría de proyectos usan la función debounce y throttle de la librería lodash, que maneja correctamente casos extremos como la cancelación, el leading/trailing edge, y el contexto de this. Implementarlas desde cero es útil para entender cómo funcionan, pero en producción lo razonable es usar una implementación probada.
