Un buscador que consulta al servidor en cada tecla, un manejador de scroll que recalcula una posición en cada píxel: estos patrones provocan cientos de llamadas inútiles y degradan la respuesta percibida. El debounce y el throttle son las dos respuestas clásicas. Se parecen, pero no resuelven el mismo problema, y confundirlos produce interfaces demasiado lentas o demasiado nerviosas.
Un mismo síntoma, dos estrategias
El punto común es la constatación: un evento se repite mucho más rápido que el trabajo que se le quiere asociar. La diferencia está en el momento elegido para actuar. El debounce espera una pausa antes de ejecutar el trabajo una sola vez. El throttle deja pasar una llamada a intervalos regulares e ignora las demás. El primero optimiza para «solo cuenta el último valor», el segundo para «basta con un ritmo controlado».
El debounce: actuar solo cuando vuelve la calma
Un debounce reinicia un temporizador en cada llamada y ejecuta la función solo si no llegó ninguna llamada nueva durante el retardo. Es el comportamiento esperado de un autocompletado: la petición parte solo cuando el usuario deja de escribir.
function debounce(fn, delay = 300) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
const search = debounce((query) => {
fetch(`/api/search?q=${encodeURIComponent(query)}`);
}, 300);
input.addEventListener('input', (e) => search(e.target.value));Ajustar el retardo es un compromiso: demasiado corto, la espera no sirve; demasiado largo, la interfaz parece inerte. Entre 200 y 400 milisegundos cubre la mayoría de los campos de entrada. Estas utilidades conviene tiparlas para que la función envuelta conserve su firma.
El throttle: garantizar un ritmo regular
Un throttle ejecuta la función de inmediato y luego bloquea cualquier llamada nueva durante una ventana dada. Sirve para flujos continuos donde importan los valores intermedios: desplazamiento, movimiento del ratón, redimensionado.
function throttle(fn, interval = 200) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last >= interval) {
last = now;
fn.apply(this, args);
}
};
}
const onScroll = throttle(() => {
updateScrollProgress(window.scrollY);
}, 200);
window.addEventListener('scroll', onScroll, { passive: true });Para el trabajo puramente visual, requestAnimationFrame ofrece un throttle natural acompasado con la tasa de refresco de la pantalla, lo que evita recalcular más a menudo de lo que el navegador pinta.
| Criterio | Debounce | Throttle |
|---|---|---|
| Momento de ejecución | Tras una pausa | A intervalos regulares |
| Número de llamadas | Una, al final | Una por ventana de tiempo |
| Valor que cuenta | El último | También los intermedios |
| Caso típico | Autocompletado, validación de formularios | Scroll, redimensionado, arrastre |
El debounce espera el final; el throttle impone un tempo. Elegir uno por otro es atacar el problema equivocado.
Errores frecuentes y buenas prácticas
La función regulada debe crearse una sola vez, no en cada render: dentro de un componente, una instancia nueva en cada ciclo reinicia el temporizador y anula todo el efecto. También hay que retirar el escuchador al desmontar y, para un debounce, prever un método de cancelación cuando el contexto desaparece, por ejemplo al cerrar un campo.
Guía de elección. Si solo importa el valor final, basta un debounce. Si hace falta una respuesta continua pero acotada, un throttle. Y si el cálculo solo alimenta la visualización, requestAnimationFrame suele hacer innecesarios ambos.
Por último, conviene conocer la implementación antes de depender de una biblioteca. Las versiones de lodash gestionan opciones finas, como disparar al inicio y al final de la ventana, pero un puñado de líneas propias cubre la mayoría de los casos y evita una dependencia para un comportamiento que se domina.
Lo que conviene recordar
Debounce y throttle no se oponen, se complementan. El primero concentra una ráfaga de eventos en una acción final; el segundo suaviza un flujo continuo en un ritmo regular. El buen reflejo es nombrar primero la necesidad —último valor o cadencia acotada— y luego elegir la técnica, no al revés. Para el trabajo de solo visualización, la planificación por fotograma sigue siendo la herramienta más económica.
En mis proyectos, nueve de cada diez regulaciones se resuelven con una docena de líneas propias, sin dependencia. Solo recurro a la biblioteca cuando necesito el disparo al inicio y al final de la ventana a la vez, y entonces no tiene sentido reescribirlo. La verdadera trampa, la que más tiempo de depuración me ha costado, sigue siendo la función recreada en cada render. — Simon Janvier
Para profundizar: la referencia de MDN sobre requestAnimationFrame.
