12k
All articles

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.

OpenReplay Team
OpenReplay Team
O Node.js agora tem debounce e throttle nativos

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() e util.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áximo limit invocações sejam iniciadas a cada interval milissegundos e, por padrão, enfileira as chamadas excedentes em vez de descartá-las.
  • Chamadas descartadas por util.throttle() são rejeitadas com ERR_THROTTLED, e o Node marca essas rejeições como tratadas.
  • Nenhuma das funções tem as opções trailing ou maxWait, 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çãoPadrãoEfeito
leadingfalseQuando true, a primeira chamada em uma janela invoca fn imediatamente
rejectOnCancelfalseQuando true, chamadas substituídas são rejeitadas com um AbortError
signalnenhumO 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ão Infinity): o número máximo de chamadas a fn em andamento ao mesmo tempo. Um slot é liberado quando o valor de retorno de fn é resolvido.
  • maxPending (padrão Infinity): o limite de tamanho da fila. Não tem efeito no modo drop.
  • overflow ('queue' por padrão, ou 'drop').
  • strict (padrão false).
  • signal: quando abortado, as chamadas pendentes e futuras são rejeitadas com um AbortError.

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:

AspectoO que o node:util fazO que mudar
Valor de retornoToda chamada retorna uma PromiseTrate a Promise com await ou .catch
Opções de bordaApenas leading; não há opções trailing nem maxWaitRemova essas chaves e repense qualquer lógica de maxWait, por exemplo, com um throttle
Modelo de throttlePermite limit inícios de chamada por interval e enfileira os excedentesUse limit igual a 1 com overflow: 'drop' para descartar os excedentes
Controle manualcancel([reason]) e flush()Chame-os diretamente no wrapper
thisDentro de fn, this é o wrapperFaç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.

DevTools for the frontend

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

We use cookies to improve your experience. By using our site, you accept cookies.