Node.js ya incluye debounce y throttle de forma nativa
Node.js 26.10.0 y 24.22.0 incorporan util.debounce y util.throttle. Conoce sus opciones, el uso de promesas y las diferencias con lodash.
Node.js 26.10.0 y 24.22.0 añaden util.debounce() y util.throttle() al módulo node:util, de modo que el código de servidor puede aplicar debounce y limitar la tasa de llamadas de las funciones sin instalar lodash ni throttle-debounce.
Si alguna vez has escrito otro wrapper más con setTimeout/clearTimeout para un vigilante de archivos, o has añadido lodash.debounce a un servicio que no usa nada más de lodash, ya no tienes que hacerlo. Ambos helpers forman ahora parte del runtime.
Este artículo explica las líneas de versiones que los incluyen, las dos firmas y sus opciones, ejemplos de servidor para cada una y lo que debes cambiar al eliminar lodash. Continúa nuestra serie sobre las API integradas de Node.js que sustituyen a paquetes de npm.
Puntos clave
util.debounce()yutil.throttle()están disponibles en Node.js 26.10.0+ y 24.22.0+. Ambas proceden del PR #65899 de nodejs/node.- Cada llamada a una función con debounce devuelve una Promise. Todas las llamadas de una misma ventana de debounce se resuelven con el resultado de una única invocación de
fn. util.throttle(fn, limit, interval)permite que comiencen como máximolimitinvocaciones por cadaintervalmilisegundos y, por defecto, encola las llamadas adicionales en lugar de descartarlas.- Las llamadas descartadas por
util.throttle()se rechazan conERR_THROTTLED, y Node marca esos rechazos como gestionados. - Ninguna de las dos funciones tiene opción
trailingnimaxWait, por lo que el código migrado desde lodash debe adaptarse al conjunto de opciones de Node.
Qué versiones de Node.js incluyen util.debounce y util.throttle
Ambas funciones aparecieron por primera vez en Node.js 26.10.0, una versión Current publicada el 22 de septiembre de 2026. Se portaron (backport) a Node.js 24.22.0 ‘Krypton’ LTS el 14 de octubre de 2026. Los dos cambios provienen del PR #65899 de James M Snell. La documentación de util indica “Added in: v26.10.0” para cada función, pero las notas de la versión 24.22.0 incluyen ambas en Notable Changes. En la línea 24.x, la versión mínima es la 24.22.0, por lo que cualquier parche 24.x posterior también las incluye.
Si publicas una librería que da soporte a versiones antiguas de Node, comprueba que util.debounce existe antes de usarla, o mantén una alternativa de respaldo.
¿Cuál es la diferencia entre debounce y throttle?
En Node.js, util.debounce() ejecuta fn una sola vez cuando dejan de llegar llamadas durante wait milisegundos, mientras que util.throttle() limita cuántas llamadas a fn pueden comenzar en cada interval. Para ver cómo funcionan desde cero, consulta debouncing y throttling para mejorar el rendimiento.
¿Cómo funciona util.debounce en Node.js?
util.debounce(fn, wait[, options]) espera a que hayan transcurrido wait milisegundos desde la llamada más reciente y, a continuación, invoca fn. Cada llamada realizada en esa ventana devuelve una Promise que se resuelve con el resultado de esa única invocación. La referencia de util.debounce enumera tres opciones:
| Opción | Valor por defecto | Efecto |
|---|---|---|
leading | false | Si es true, la primera llamada de una ventana invoca fn de inmediato |
rejectOnCancel | false | Si es true, las llamadas reemplazadas por otras posteriores se rechazan con un AbortError |
signal | ninguno | Al abortarla, se cancelan las llamadas pendientes y se bloquean las futuras. Una señal ya abortada hace que debounce() lance una excepción |
La función devuelta también expone cancel([reason]), flush(), pending, pendingCount, ref() y unref().
Requiere Node.js ≥ 24.22.0 o ≥ 26.10.0.
import { debounce } from 'node:util';
const save = debounce(async (value) => `saved:${value}`, 100);
const calls = [save('a'), save('b'), save('c')];
console.log(save.pendingCount); // 3
console.log(await Promise.all(calls)); // [ 'saved:c', 'saved:c', 'saved:c' ]
fn solo recibe los argumentos de la llamada más reciente. Para recopilar todos los eventos de una ráfaga, almacénalos fuera de fn y léelos cuando se ejecute. El vigilante de archivos siguiente lo hace con un Set:
import { watch } from 'node:fs';
import { debounce } from 'node:util';
const changed = new Set();
const rebuild = debounce(async () => {
const batch = [...changed];
changed.clear();
console.log('rebuilding for', batch);
return batch;
}, 200).unref();
const watcher = watch('./src', (eventType, filename) => {
changed.add(filename);
rebuild().catch(console.error);
});
process.on('SIGINT', () => {
rebuild.flush();
watcher.close();
});
Llamar a unref() sobre una función con debounce o throttle permite que el proceso finalice aunque su temporizador siga pendiente. Esto resulta idóneo para vigilantes de archivos y volcadores de logs, que nunca deberían bloquear el apagado. unref() devuelve la propia función, así que puedes encadenarla a la llamada a debounce(). Durante el apagado, flush() ejecuta el último lote para que no se pierda.
A diferencia de los rechazos ERR_THROTTLED de throttle, debounce no marca todos los rechazos como gestionados. El código fuente de Node marca la promesa de una llamada como gestionada en cuanto una llamada posterior la reemplaza, pero no así la promesa de la llamada más reciente. Si cancel() o una signal abortada rechaza esa promesa y nada la captura, el proceso sufre un unhandled rejection. Para llamadas de tipo fire-and-forget, como la del callback del vigilante anterior, añade un .catch.
¿Cómo limita util.throttle la tasa de llamadas?
util.throttle(fn, limit, interval[, options]) permite que comiencen como máximo limit invocaciones de fn en cada interval milisegundos. limit debe ser mayor que 0. Por defecto, las llamadas que superan el límite se encolan por orden de llegada en lugar de descartarse. La referencia de util.throttle define estas opciones:
concurrency(por defectoInfinity): el número máximo de llamadas afnen curso simultáneamente. Un hueco se libera cuando el valor de retorno defnse resuelve.maxPending(por defectoInfinity): el tamaño máximo de la cola. No tiene efecto en el modo de descarte.overflow('queue'por defecto, o'drop').strict(por defectofalse).signal: al abortarla, las llamadas pendientes y futuras se rechazan con unAbortError.
La capacidad de tasa se consume cuando fn comienza, no cuando una llamada entra en la cola. La función también expone cancel([reason]), hasImmediateCapacity(), pending, pendingCount, activeCount, ref() y unref().
import { setTimeout as sleep } from 'node:timers/promises';
import { throttle } from 'node:util';
const callApi = throttle(async (id) => {
await sleep(50); // stand-in for a real request
return { id };
}, 5, 1_000, { concurrency: 2, maxPending: 100 });
const calls = Array.from({ length: 12 }, (_, i) => callApi(i));
console.log('active:', callApi.activeCount, 'queued:', callApi.pendingCount);
console.log(await Promise.all(calls)); // results in call order
Por defecto, util.throttle() cuenta las invocaciones en ventanas fijas. Esto puede permitir que dos grupos de llamadas comiencen muy próximos entre sí en el límite entre ventanas. Si estableces strict: true, el límite se aplica en cualquier intervalo deslizante.
Para ejecutar una función como máximo una vez por segundo y descartar las llamadas adicionales en lugar de encolarlas, usa throttle(fn, 1, 1000, { overflow: 'drop' }):
import { throttle } from 'node:util';
const ping = throttle(async () => 'pong', 1, 1_000, { overflow: 'drop' });
const results = await Promise.allSettled([ping(), ping(), ping()]);
for (const r of results) {
console.log(r.status === 'fulfilled' ? r.value : r.reason.code);
}
// pong, ERR_THROTTLED, ERR_THROTTLED
if (ping.hasImmediateCapacity()) ping(); // gate instead of catching
Las llamadas que util.throttle() descarta se rechazan con un error ERR_THROTTLED. Una llamada se descarta porque overflow vale 'drop' o porque la cola de maxPending está llena. Node marca estas promesas como gestionadas, y las llamadas descartadas no consumen capacidad de tasa ni de concurrencia. cancel() vacía las llamadas encoladas y reinicia la ventana, pero no detiene las invocaciones que ya han comenzado.
¿Cómo sustituir lodash.debounce y lodash.throttle?
util.debounce() y util.throttle() pueden sustituir a lodash.debounce y lodash.throttle en la mayor parte del código de servidor, pero no siguen el mismo modelo que lodash. El throttle de Node limita cuántas llamadas pueden comenzar por intervalo y encola el resto. El throttle de lodash permite como máximo una llamada por cada wait. Migra tu código utilizando únicamente el conjunto de opciones de node:util:
| Aspecto | Qué hace node:util | Qué cambiar |
|---|---|---|
| Valor de retorno | Cada llamada devuelve una Promise | Gestiona la Promise con await o .catch |
| Opciones de flanco | Solo leading; no existen las opciones trailing ni maxWait | Elimina esas claves y replantea cualquier lógica de maxWait, por ejemplo con un throttle |
| Modelo de throttle | Permite que comiencen limit llamadas por interval y encola las adicionales | Usa un limit de 1 junto con overflow: 'drop' para descartar las llamadas sobrantes |
| Control manual | cancel([reason]) y flush() | Llámalos directamente sobre el wrapper |
this | Dentro de fn, this es el wrapper | Vincula (bind) los métodos previamente |
// before: hand-rolled
let timer;
const saveLater = (doc) => {
clearTimeout(timer);
timer = setTimeout(() => store.save(doc), 500);
};
// after
import { debounce } from 'node:util';
const saveLater = debounce(store.save.bind(store), 500);
saveLater(doc).catch(console.error);
// on shutdown: saveLater.flush(); or discard: saveLater.cancel();
Cuándo mantener lodash o un helper propio
Mantén lodash, throttle-debounce o un helper escrito a mano en lugar de node:util en dos casos:
- Código de navegador.
node:utiles un módulo de servidor. Los bundles de front-end siguen necesitando su propio helper; consulta estrategias de debounce en React. - Versiones de Node anteriores a la 24.22.0. Esto incluye los parches 24.x anteriores y todas las líneas de versiones más antiguas. Los proyectos que deban dar soporte a esas versiones deberían conservar la dependencia o añadir una comprobación de disponibilidad.
Conclusión
En Node.js 26.10.0+ y 24.22.0+, el debounce y la limitación de tasa forman parte de node:util. Ambas funciones devuelven Promises e incorporan cancel() y unref(), y la función con debounce añade además flush(). Como siguiente paso, localiza dónde usa tu código de servidor lodash.debounce o lodash.throttle y sustituye un vigilante de archivos o un cliente saliente por la versión integrada. Vincula los métodos antes de envolverlos y añade un .catch a cada llamada que no esperes con await.
Preguntas frecuentes
¿util.debounce con leading: true ejecuta la función dos veces?
Solo si llegan más llamadas durante la ventana. Con leading establecido en true, la primera llamada de una ventana de debounce invoca fn de inmediato. Una segunda invocación, en el flanco final, solo se produce si la función con debounce se volvió a llamar antes de que terminara el periodo de espera. Por tanto, una única llamada aislada ejecuta fn exactamente una vez, en el flanco inicial, y no existe ninguna opción trailing que desactivar.
¿util.debounce espera a que termine una función asíncrona antes de abrir una nueva ventana?
No. Tras una invocación en el flanco final, util.debounce puede abrir una nueva ventana de debounce aunque la promesa devuelta por fn aún no se haya resuelto, por lo que dos ejecuciones de una fn asíncrona lenta pueden solaparse. Para evitar ejecuciones solapadas, usa util.throttle con concurrency establecido en 1. Su hueco de concurrencia solo se libera cuando el valor de retorno de fn se resuelve.
¿Cómo compruebo la disponibilidad de util.debounce sin que falle en versiones antiguas de Node.js?
Importa el módulo completo y comprueba la propiedad: import util from 'node:util', y después verifica typeof util.debounce === 'function' antes de usarla, recurriendo a un helper de userland si no existe. En ESM, una importación con nombre de una exportación inexistente falla con un SyntaxError al enlazar el módulo, por lo que el código nunca llega a ejecutar la comprobación.
¿Qué ocurre con las llamadas encoladas en util.throttle cuando se aborta su AbortSignal?
Se rechazan. Cuando se aborta la señal pasada a util.throttle, las llamadas pendientes y futuras se rechazan con un AbortError, y el reason de la señal se establece como cause del error. Las invocaciones que ya han comenzado siguen ejecutándose. A diferencia de los rechazos ERR_THROTTLED, la documentación no indica que estos rechazos AbortError se marquen como gestionados, así que añade un manejador catch a cada llamada que no esperes con await.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k