O Node.js agora tem debounce e throttle nativos
Node.js 26.10.0 e 24.22.0 adicionam util.debounce e util.throttle. Veja opções, comportamento das Promises e diferenças em relação ao lodash.
O Node.js 26.10.0 e o 24.22.0 adicionam util.debounce() e util.throttle() ao módulo node:util. Com isso, o código de servidor pode aplicar debounce e limitar a taxa de execução de funções sem instalar lodash ou throttle-debounce.
Se você já escreveu mais um wrapper com setTimeout/clearTimeout para um file watcher, ou adicionou lodash.debounce a um serviço que não usa mais nada do lodash, isso não é mais necessário. Os dois helpers agora fazem parte do runtime.
Este artigo aborda as linhas de release, as duas assinaturas e suas opções, exemplos de uso no servidor para cada uma e o que você precisa mudar ao remover o lodash. Ele dá continuidade à nossa série sobre APIs nativas do Node.js que substituem pacotes npm.
Principais pontos
util.debounce()eutil.throttle()estão disponíveis no Node.js 26.10.0+ e 24.22.0+. Ambas vieram do PR #65899 do nodejs/node.- Toda chamada a uma função com debounce retorna uma Promise. Todas as chamadas em uma mesma janela de debounce são resolvidas com o resultado de uma única invocação de
fn. util.throttle(fn, limit, interval)permite que no máximolimitinvocações sejam iniciadas a cadaintervalmilissegundos e, por padrão, enfileira as chamadas excedentes em vez de descartá-las.- Chamadas descartadas por
util.throttle()são rejeitadas comERR_THROTTLED, e o Node marca essas rejeições como tratadas. - Nenhuma das funções tem as opções
trailingoumaxWait, então código portado do lodash precisa ser adaptado ao conjunto de opções do Node.
Quais versões do Node.js incluem util.debounce e util.throttle
As duas funções foram lançadas primeiro no Node.js 26.10.0, uma release Current de 22 de setembro de 2026. Elas receberam backport para o Node.js 24.22.0 ‘Krypton’ LTS em 14 de outubro de 2026. Ambas as mudanças vêm do PR #65899, de James M Snell. A documentação do util mostra “Added in: v26.10.0” para cada função, mas as notas de release do 24.22.0 listam as duas em Notable Changes. Na linha 24.x, a versão mínima é a 24.22.0, então qualquer patch 24.x posterior também as inclui.
Se você publica uma biblioteca que dá suporte a versões mais antigas do Node, verifique se util.debounce existe antes de usá-la ou mantenha um fallback.
Qual é a diferença entre debounce e throttle?
No Node.js, util.debounce() executa fn uma única vez depois que as chamadas param de chegar por wait milissegundos, enquanto util.throttle() limita quantas chamadas a fn podem ser iniciadas em cada interval. Para entender o mecanismo implementado do zero, veja debouncing e throttling para performance.
Como o util.debounce funciona no Node.js?
util.debounce(fn, wait[, options]) aguarda até que wait milissegundos tenham se passado desde a chamada mais recente e então invoca fn. Toda chamada feita nessa janela retorna uma Promise que é resolvida com o resultado dessa única invocação. A referência de util.debounce lista três opções:
| Opção | Padrão | Efeito |
|---|---|---|
leading | false | Quando true, a primeira chamada em uma janela invoca fn imediatamente |
rejectOnCancel | false | Quando true, chamadas substituídas são rejeitadas com um AbortError |
signal | nenhum | O abort cancela as chamadas pendentes e bloqueia as futuras. Um signal já abortado faz debounce() lançar uma exceção |
A função retornada também tem cancel([reason]), flush(), pending, pendingCount, ref() e unref().
Requer Node.js ≥ 24.22.0 ou ≥ 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 recebe apenas os argumentos da chamada mais recente. Para coletar todos os eventos de uma rajada, armazene-os fora de fn e leia-os quando ela for executada. O file watcher abaixo faz isso com um 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();
});
Chamar unref() em uma função com debounce ou throttle permite que o processo seja encerrado enquanto o timer ainda está pendente. Isso é ideal para file watchers e log flushers, que nunca devem bloquear o shutdown. unref() retorna a própria função, então você pode encadeá-la na chamada a debounce(). No shutdown, flush() executa o último lote para que ele não seja perdido.
Diferentemente das rejeições ERR_THROTTLED do throttle, o debounce não marca todas as rejeições como tratadas. O código-fonte do Node marca a promise de uma chamada como tratada quando uma chamada posterior a substitui, mas a promise da chamada mais recente não é marcada. Se cancel() ou um signal abortado rejeitar essa promise e nada a capturar, o processo terá uma unhandled rejection. Para chamadas fire-and-forget, como o callback do watcher acima, adicione um .catch.
Como o util.throttle limita a taxa de chamadas?
util.throttle(fn, limit, interval[, options]) permite que no máximo limit invocações de fn sejam iniciadas a cada interval milissegundos. limit deve ser maior que 0. Por padrão, as chamadas que excedem o limite são enfileiradas por ordem de chegada em vez de descartadas. A referência de util.throttle define estas opções:
concurrency(padrãoInfinity): o número máximo de chamadas afnem andamento ao mesmo tempo. Um slot é liberado quando o valor de retorno defné resolvido.maxPending(padrãoInfinity): o limite de tamanho da fila. Não tem efeito no modo drop.overflow('queue'por padrão, ou'drop').strict(padrãofalse).signal: quando abortado, as chamadas pendentes e futuras são rejeitadas com umAbortError.
A capacidade de taxa é consumida quando fn é iniciada, e não quando uma chamada entra na fila. A função também expõe cancel([reason]), hasImmediateCapacity(), pending, pendingCount, activeCount, ref() e 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 padrão, util.throttle() conta as invocações em janelas fixas. Isso pode permitir que dois grupos de chamadas sejam iniciados muito próximos um do outro na fronteira entre janelas. Definir strict: true faz com que o limite seja aplicado em qualquer intervalo deslizante.
Para executar uma função no máximo uma vez por segundo e descartar as chamadas excedentes em vez de enfileirá-las, use 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
Chamadas descartadas por util.throttle() são rejeitadas com um erro ERR_THROTTLED. Uma chamada é descartada porque overflow é 'drop' ou porque a fila de maxPending está cheia. O Node marca essas promises como tratadas, e as chamadas descartadas não consomem capacidade de taxa nem de concorrência. cancel() limpa as chamadas enfileiradas e reinicia a janela, mas não interrompe invocações que já começaram.
Como substituir lodash.debounce e lodash.throttle?
util.debounce() e util.throttle() podem substituir lodash.debounce e lodash.throttle na maior parte do código de servidor, mas não seguem o mesmo modelo do lodash. O throttle do Node limita quantas chamadas podem ser iniciadas por intervalo e enfileira as demais. O throttle do lodash permite no máximo uma chamada a cada wait. Faça a migração do seu código usando apenas o conjunto de opções do node:util:
| Aspecto | O que o node:util faz | O que mudar |
|---|---|---|
| Valor de retorno | Toda chamada retorna uma Promise | Trate a Promise com await ou .catch |
| Opções de borda | Apenas leading; não há opções trailing nem maxWait | Remova essas chaves e repense qualquer lógica de maxWait, por exemplo, com um throttle |
| Modelo de throttle | Permite limit inícios de chamada por interval e enfileira os excedentes | Use limit igual a 1 com overflow: 'drop' para descartar os excedentes |
| Controle manual | cancel([reason]) e flush() | Chame-os diretamente no wrapper |
this | Dentro de fn, this é o wrapper | Faça o bind dos métodos antes |
// 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();
Quando manter o lodash ou um helper próprio
Mantenha o lodash, o throttle-debounce ou um helper escrito à mão em vez do node:util em dois casos:
- Código de navegador. O
node:utilé um módulo do lado do servidor. Bundles de front-end ainda precisam de um helper próprio; veja estratégias de debounce no React. - Versões do Node anteriores à 24.22.0. Isso inclui patches 24.x anteriores e todas as linhas de release mais antigas. Projetos que precisam dar suporte a essas versões devem manter a dependência ou adicionar uma verificação de recurso.
Conclusão
No Node.js 26.10.0+ e 24.22.0+, debounce e limitação de taxa fazem parte do node:util. As duas funções retornam Promises e já vêm com cancel() e unref(), e a função com debounce ainda adiciona flush(). Como próximo passo, identifique onde o seu código de servidor usa lodash.debounce ou lodash.throttle e substitua um file watcher ou um cliente de saída pela versão nativa. Faça o bind de qualquer método antes de envolvê-lo e adicione um .catch a toda chamada que você não aguardar com await.
Perguntas frequentes
O util.debounce com leading: true executa a função duas vezes?
Somente se outras chamadas chegarem durante a janela. Com leading definido como true, a primeira chamada em uma janela de debounce invoca fn imediatamente. Uma segunda invocação, no final da janela, só acontece se a função com debounce for chamada novamente antes do fim do período de espera. Portanto, uma chamada isolada executa fn exatamente uma vez, no início da janela, e não existe uma opção trailing para desativar.
O util.debounce espera uma função assíncrona terminar antes de iniciar uma nova janela?
Não. Após uma invocação no final da janela, o util.debounce pode abrir uma nova janela de debounce mesmo que a promise retornada por fn ainda não tenha sido resolvida, então duas execuções de uma fn assíncrona lenta podem se sobrepor. Para evitar execuções sobrepostas, use util.throttle com concurrency definido como 1. O slot de concorrência só é liberado quando o valor de retorno de fn é resolvido.
Como fazer feature detection do util.debounce sem quebrar em versões antigas do Node.js?
Importe o módulo inteiro e verifique a propriedade: use import util from 'node:util' e teste typeof util.debounce === 'function' antes de usá-la, recorrendo a um helper em userland caso ela não exista. Em ESM, um named import de um export inexistente falha com um SyntaxError na etapa de linking do módulo, então o código nem chega a executar a verificação.
O que acontece com as chamadas enfileiradas do util.throttle quando o AbortSignal é abortado?
Elas são rejeitadas. Quando o signal passado para util.throttle é abortado, as chamadas pendentes e futuras são rejeitadas com um AbortError, e o reason do signal é definido como o cause do erro. Invocações que já começaram continuam em execução. Diferentemente das rejeições ERR_THROTTLED, a documentação não diz que essas rejeições AbortError são marcadas como tratadas, então adicione um handler de catch a toda chamada que você não aguardar com 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