Articulo publicado el
Optimiza el rendimiento de tu aplicación limitando cuántas veces se ejecutan listeners de eventos y funciones costosas
Debounce y Throttle son técnicas de programación que controlan la frecuencia con la que se ejecuta una función. Ambas resuelven el mismo problema: limitar el número de veces que se llama a una función costosa o que genera muchas ejecuciones innecesarias, pero cada una toma un enfoque diferente.
Cuando trabajas con eventos como scroll, resize, mousemove o búsqueda en tiempo real, el navegador puede dispararlos cientos de veces por segundo. Sin debounce o throttle, tu aplicación puede ralentizarse significativamente porque estás ejecutando lógica cara (validaciones, peticiones HTTP, cálculos DOM) demasiadas veces.
Imagina una búsqueda en tiempo real donde cada tecla que presiona el usuario dispara una petición al servidor:
// Sin control - ineficiente
const input = document.getElementById("search");
input.addEventListener("input", (e) => {
const query = e.target.value;
// Esto se ejecuta en CADA tecla presionada
fetch(`/api/search?q=${query}`)
.then((res) => res.json())
.then((data) => console.log(data));
});
Si un usuario escribe "javascript" (10 caracteres), esto dispara 10 peticiones HTTP al servidor. Eso es ineficiente, caro y puede generar problemas de concurrencia en tu backend.
Aunque ambas técnicas limitan ejecuciones, lo hacen de manera distinta:
Debounce retrasa la ejecución de una función hasta que ha pasado un tiempo específico sin que se llame. Es ideal para acciones que ocurren después de que el usuario "termina" de hacer algo.
Caso de uso: Campo de búsqueda, validación de formulario, guardado automático.
// Debounce: espera 300ms sin actividad antes de ejecutar
function debounce(func, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func(...args);
}, delay);
};
}
const input = document.getElementById("search");
const debouncedSearch = debounce((e) => {
const query = e.target.value;
fetch(`/api/search?q=${query}`)
.then((res) => res.json())
.then((data) => console.log(data));
}, 300);
input.addEventListener("input", debouncedSearch);
En este ejemplo, si el usuario escribe "javascript" (10 caracteres) a velocidad normal, solo se dispara 1 petición 300ms después de que deja de escribir.
Throttle ejecuta una función como máximo una vez cada X milisegundos, independientemente de cuántas veces se dispare el evento. Es ideal para acciones que necesitan ejecutarse regularmente pero con un límite máximo.
Caso de uso: Scroll infinito, redimensionamiento de ventana, tracking de mouse.
// Throttle: ejecuta máximo cada 300ms
function throttle(func, delay) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= delay) {
func(...args);
lastCall = now;
}
};
}
window.addEventListener(
"scroll",
throttle(() => {
console.log("Scroll detectado");
// Lógica costosa
}, 300)
);
En este ejemplo, aunque el usuario haga scroll rápidamente (disparando el evento cientos de veces), la función se ejecuta máximo cada 300ms.
Usa debounce cuando necesites ejecutar una función después de que la actividad del usuario se haya detenido:
const searchInput = document.getElementById("search");
const debouncedSearch = debounce((e) => {
const query = e.target.value;
if (query.length === 0) {
clearResults();
return;
}
searchAPI(query).then((results) => displayResults(results));
}, 500);
searchInput.addEventListener("input", debouncedSearch);
const emailInput = document.getElementById("email");
const debouncedValidate = debounce((e) => {
const email = e.target.value;
if (!isValidEmail(email)) {
showError("Email inválido");
return;
}
checkEmailExists(email).then((exists) => {
if (exists) {
showError("Email ya registrado");
} else {
showSuccess("Email disponible");
}
});
}, 400);
emailInput.addEventListener("blur", debouncedValidate);
emailInput.addEventListener("input", debouncedValidate);
const editor = document.getElementById("editor");
const debouncedSave = debounce(() => {
const content = editor.value;
saveToServer(content).then(() => {
showNotification("Guardado correctamente");
});
}, 2000);
editor.addEventListener("input", debouncedSave);
Usa throttle cuando necesites limitar la frecuencia de una función que se ejecuta continuamente durante la actividad del usuario:
const throttledScroll = throttle(() => {
const scrollTop = window.scrollY;
if (scrollTop > 100) {
showBackToTopButton();
} else {
hideBackToTopButton();
}
}, 100);
window.addEventListener("scroll", throttledScroll);
const throttledTracking = throttle((e) => {
const mouseX = e.clientX;
const mouseY = e.clientY;
// Registrar posición en servidor (máximo cada 100ms)
trackMousePosition(mouseX, mouseY);
}, 100);
document.addEventListener("mousemove", throttledTracking);
const throttledResize = throttle(() => {
const width = window.innerWidth;
const height = window.innerHeight;
// Recalcular layout (máximo cada 250ms)
recalculateLayout(width, height);
}, 250);
window.addEventListener("resize", throttledResize);
A veces necesitas que la función se ejecute inmediatamente en la primera llamada, y luego espere a que pare la actividad:
function debounceWithLeading(func, delay) {
let timeoutId;
let lastCall = 0;
return function (...args) {
const now = Date.now();
const isFirstCall = lastCall === 0;
// Ejecutar inmediatamente en primera llamada
if (isFirstCall) {
func(...args);
lastCall = now;
}
clearTimeout(timeoutId);
// Ejecutar de nuevo al final si hay actividad
timeoutId = setTimeout(() => {
if (now - lastCall >= delay) {
func(...args);
lastCall = now;
}
}, delay);
};
}
A veces quieres asegurar que se ejecute una última vez después de que la actividad termina:
function throttleWithTrailing(func, delay) {
let lastCall = 0;
let timeoutId;
return function (...args) {
const now = Date.now();
if (now - lastCall >= delay) {
func(...args);
lastCall = now;
}
clearTimeout(timeoutId);
// Ejecutar una última vez al final
timeoutId = setTimeout(() => {
func(...args);
lastCall = now;
}, delay);
};
}
Para un evento que se dispara 100 veces en 1 segundo:
Búsqueda sin debounce: 50 peticiones HTTP por búsqueda Búsqueda con debounce (500ms): 1 petición por búsqueda
Eso es 50x menos carga en el servidor.
// ❌ INCORRECTO: Debounce en scroll
window.addEventListener(
"scroll",
debounce(() => {
loadMoreContent();
}, 500)
);
// ✅ CORRECTO: Throttle en scroll
window.addEventListener(
"scroll",
throttle(() => {
loadMoreContent();
}, 300)
);
El scroll infinito necesita ejecutarse regularmente mientras se hace scroll, no al final.
// ❌ INCORRECTO en React
function SearchComponent() {
const handleSearch = debounce((query) => {
searchAPI(query);
}, 500);
return <input onChange={handleSearch} />;
}
// ✅ CORRECTO: Memoizar la función debounceada
function SearchComponent() {
const handleSearch = useMemo(
() =>
debounce((query) => {
searchAPI(query);
}, 500),
[]
);
return <input onChange={handleSearch} />;
}
Si el componente se desmonta, los timeouts quedan pendientes:
// ✅ CORRECTO: Limpiar en unmount
function useDebouncedSearch(callback, delay) {
const debouncedRef = useRef();
useEffect(() => {
debouncedRef.current = debounce(callback, delay);
return () => {
// Limpiar timeout al desmontar
debouncedRef.current.cancel?.();
};
}, [callback, delay]);
return debouncedRef.current;
}
Para proyectos grandes, considera usar librerías bien mantenidas:
_.debounce() y _.throttle()useDebouncedValue o useThrottledCallbackDebounce es para esperar a que la actividad termine; Throttle es para limitar la frecuencia regular.
Dominar estas técnicas es esencial para construir aplicaciones frontend rápidas y responsivas. Úsalas estratégicamente para mejorar el rendimiento, reducir la carga del servidor y proporcionar una mejor experiencia de usuario.
Estos articulos pueden interesarte si te gusto este articulo.
Cómo reducir listeners y mejorar el rendimiento usando bubbling de eventos
Maneja eventos en múltiples elementos con un único listener y mejora el rendimiento de tu aplicación
Aprende a usar los atributos data-* para guardar información en elementos HTML y accederla desde JavaScript de forma segura y semántica