12k
All articles

Node.js intègre désormais nativement le debounce et le throttle

Node.js 26.10.0 et 24.22.0 ajoutent util.debounce et util.throttle. Découvrez leurs options, leur gestion des promesses et leurs différences avec lodash.

OpenReplay Team
OpenReplay Team
Node.js intègre désormais nativement le debounce et le throttle

Node.js 26.10.0 et 24.22.0 ajoutent util.debounce() et util.throttle() au module node:util. Le code serveur peut ainsi appliquer un debounce ou limiter le débit d’appel des fonctions sans installer lodash ni throttle-debounce.

Si vous avez déjà réécrit un énième wrapper setTimeout/clearTimeout pour un observateur de fichiers, ou ajouté lodash.debounce à un service qui n’utilise rien d’autre de lodash, ce n’est plus nécessaire. Les deux utilitaires font désormais partie du runtime.

Cet article présente les lignes de versions concernées, les deux signatures et leurs options, des exemples côté serveur pour chacune, ainsi que les modifications à apporter lorsque vous supprimez lodash. Il s’inscrit dans notre série consacrée aux API natives de Node.js qui remplacent des paquets npm.

Points clés

  • util.debounce() et util.throttle() sont disponibles dans Node.js 26.10.0+ et 24.22.0+. Les deux proviennent de la PR nodejs/node #65899.
  • Chaque appel à une fonction avec debounce renvoie une Promise. Tous les appels d’une même fenêtre de debounce se résolvent avec le résultat d’une seule invocation de fn.
  • util.throttle(fn, limit, interval) autorise au maximum limit invocations démarrées par période de interval millisecondes et, par défaut, met les appels excédentaires en file d’attente au lieu de les abandonner.
  • Les appels abandonnés par util.throttle() sont rejetés avec ERR_THROTTLED, et Node marque ces rejets comme gérés.
  • Aucune des deux fonctions ne propose d’option trailing ou maxWait : le code porté depuis lodash doit donc être adapté au jeu d’options de Node.

Quelles versions de Node.js intègrent util.debounce et util.throttle

Les deux fonctions sont apparues pour la première fois dans Node.js 26.10.0, une version Current publiée le 22 septembre 2026. Elles ont été rétroportées dans Node.js 24.22.0 ‘Krypton’ LTS le 14 octobre 2026. Les deux modifications proviennent de la PR #65899 de James M Snell. La documentation de util indique « Added in: v26.10.0 » pour chaque fonction, mais les notes de version de la 24.22.0 les mentionnent toutes deux dans la section Notable Changes. Sur la ligne 24.x, la version minimale est la 24.22.0 : tout correctif 24.x ultérieur les inclut donc également.

Si vous publiez une bibliothèque compatible avec d’anciennes versions de Node, vérifiez que util.debounce existe avant de l’utiliser, ou conservez une solution de repli.

Quelle est la différence entre debounce et throttle ?

Dans Node.js, util.debounce() exécute fn une seule fois, lorsque aucun appel n’est survenu pendant wait millisecondes, tandis que util.throttle() limite le nombre d’appels à fn pouvant démarrer au cours de chaque interval. Pour le fonctionnement détaillé et une implémentation de zéro, consultez debouncing et throttling pour la performance.

Comment fonctionne util.debounce dans Node.js ?

util.debounce(fn, wait[, options]) attend que wait millisecondes se soient écoulées depuis l’appel le plus récent, puis invoque fn. Chaque appel effectué pendant cette fenêtre renvoie une Promise qui se résout avec le résultat de cette unique invocation. La référence de util.debounce liste trois options :

OptionValeur par défautEffet
leadingfalseSi true, le premier appel d’une fenêtre invoque fn immédiatement
rejectOnCancelfalseSi true, les appels supplantés sont rejetés avec une AbortError
signalaucuneL’abandon annule les appels en attente et bloque les appels futurs. Un signal déjà abandonné fait lever une exception à debounce()

La fonction renvoyée expose également cancel([reason]), flush(), pending, pendingCount, ref() et unref().

Nécessite 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 ne reçoit que les arguments de l’appel le plus récent. Pour collecter tous les événements d’une rafale, stockez-les en dehors de fn et lisez-les lors de son exécution. L’observateur de fichiers ci-dessous procède ainsi avec 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();
});

Appeler unref() sur une fonction avec debounce ou throttle permet au processus de se terminer même si son timer est encore en attente. C’est adapté aux observateurs de fichiers et aux mécanismes de vidage de logs, qui ne doivent jamais bloquer l’arrêt. unref() renvoie la fonction : vous pouvez donc l’enchaîner directement sur l’appel à debounce(). À l’arrêt, flush() exécute le dernier lot afin qu’il ne soit pas perdu.

Contrairement aux rejets ERR_THROTTLED de throttle, debounce ne marque pas tous les rejets comme gérés. Le code source de Node marque la promesse d’un appel comme gérée dès qu’un appel ultérieur la supplante, mais pas celle de l’appel le plus récent. Si cancel() ou un signal abandonné rejette cette promesse et que rien ne l’intercepte, le processus subit un rejet non géré (unhandled rejection). Pour les appels de type « fire-and-forget », comme le callback de l’observateur ci-dessus, ajoutez un .catch.

Comment util.throttle limite-t-il le débit des appels ?

util.throttle(fn, limit, interval[, options]) autorise au maximum limit invocations de fn à démarrer au cours de chaque période de interval millisecondes. limit doit être supérieur à 0. Par défaut, les appels dépassant la limite sont mis en file d’attente dans leur ordre d’arrivée au lieu d’être abandonnés. La référence de util.throttle définit les options suivantes :

  • concurrency (par défaut Infinity) : le nombre maximal d’appels à fn en cours simultanément. Un emplacement est libéré lorsque la valeur de retour de fn est résolue.
  • maxPending (par défaut Infinity) : la taille maximale de la file d’attente. Cette option est sans effet en mode drop.
  • overflow ('queue' par défaut, ou 'drop').
  • strict (par défaut false).
  • signal : en cas d’abandon, les appels en attente et futurs sont rejetés avec une AbortError.

La capacité de débit est consommée au démarrage de fn, et non lorsqu’un appel rejoint la file d’attente. La fonction expose également cancel([reason]), hasImmediateCapacity(), pending, pendingCount, activeCount, ref() et 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

Par défaut, util.throttle() comptabilise les invocations par fenêtres fixes. Cela peut permettre à deux groupes d’appels de démarrer à peu d’intervalle de part et d’autre de la limite d’une fenêtre. Définir strict: true impose au contraire la limite sur chaque intervalle glissant.

Pour exécuter une fonction au plus une fois par seconde et abandonner les appels excédentaires au lieu de les mettre en file d’attente, utilisez 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

Les appels abandonnés par util.throttle() sont rejetés avec une erreur ERR_THROTTLED. Un appel est abandonné soit parce que overflow vaut 'drop', soit parce que la file d’attente maxPending est pleine. Node marque ces promesses comme gérées, et les appels abandonnés ne consomment aucune capacité de débit ni de concurrence. cancel() vide la file d’attente et réinitialise la fenêtre, mais n’interrompt pas les invocations déjà démarrées.

Comment remplacer lodash.debounce et lodash.throttle ?

util.debounce() et util.throttle() peuvent remplacer lodash.debounce et lodash.throttle dans la plupart du code serveur, mais ils ne reposent pas sur le même modèle que lodash. Le throttle de Node limite le nombre d’appels pouvant démarrer par intervalle et met les autres en file d’attente. Le throttle de lodash, lui, autorise au maximum un appel par période wait. Portez votre code en vous appuyant uniquement sur le jeu d’options de node:util :

AspectComportement de node:utilCe qu’il faut modifier
Valeur de retourChaque appel renvoie une PromiseGérer la Promise avec await ou .catch
Options de déclenchementUniquement leading ; pas d’options trailing ni maxWaitSupprimer ces clés et repenser toute logique maxWait, par exemple avec un throttle
Modèle de throttleAutorise limit démarrages d’appels par interval et met les excédents en file d’attenteUtiliser un limit de 1 avec overflow: 'drop' pour ignorer les appels excédentaires
Contrôle manuelcancel([reason]) et flush()Les appeler directement sur le wrapper
thisDans fn, this désigne le wrapperLier (bind) les méthodes au préalable
// 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();

Quand conserver lodash ou un utilitaire maison

Conservez lodash, throttle-debounce ou un utilitaire écrit à la main plutôt que node:util dans deux cas :

  • Code navigateur. node:util est un module côté serveur. Les bundles front-end ont toujours besoin de leur propre utilitaire ; consultez les stratégies de debounce avec React.
  • Versions de Node antérieures à 24.22.0. Cela inclut les correctifs 24.x précédents et toutes les lignes de versions plus anciennes. Les projets devant prendre en charge ces versions doivent conserver la dépendance ou ajouter une détection de fonctionnalité.

Conclusion

Sur Node.js 26.10.0+ et 24.22.0+, le debounce et la limitation de débit font partie de node:util. Les deux fonctions renvoient des Promises et intègrent nativement cancel() et unref(), la fonction avec debounce ajoutant en plus flush(). Prochaine étape : repérez les endroits où votre code serveur utilise lodash.debounce ou lodash.throttle, et remplacez un observateur de fichiers ou un client sortant par l’API native. Liez les méthodes avant de les encapsuler, et ajoutez un .catch à chaque appel que vous n’attendez pas avec await.

FAQ

util.debounce avec leading: true exécute-t-il la fonction deux fois ?

Uniquement si d'autres appels surviennent pendant la fenêtre. Lorsque leading vaut true, le premier appel d'une fenêtre de debounce invoque fn immédiatement. Une seconde invocation, en fin de fenêtre, n'a lieu que si la fonction avec debounce a de nouveau été appelée avant la fin du délai d'attente. Un appel unique et isolé exécute donc fn exactement une fois, en début de fenêtre, et il n'existe aucune option trailing à désactiver.

util.debounce attend-il la fin d'une fonction asynchrone avant d'ouvrir une nouvelle fenêtre ?

Non. Après une invocation en fin de fenêtre, util.debounce peut ouvrir une nouvelle fenêtre de debounce même si la promesse renvoyée par fn n'est pas encore résolue : deux exécutions d'une fn asynchrone lente peuvent donc se chevaucher. Pour éviter ce chevauchement, utilisez util.throttle avec concurrency défini à 1. Son emplacement de concurrence n'est libéré que lorsque la valeur de retour de fn est résolue.

Comment détecter la présence de util.debounce sans provoquer d'erreur sur les anciennes versions de Node.js ?

Importez le module entier et vérifiez la propriété : import util from 'node:util', puis testez typeof util.debounce === 'function' avant de l'utiliser, et rabattez-vous sur un utilitaire userland s'il est absent. En ESM, un import nommé d'un export inexistant échoue avec une SyntaxError lors de la liaison du module : le code n'a donc jamais l'occasion d'exécuter la vérification.

Que deviennent les appels en file d'attente de util.throttle lorsque son AbortSignal est abandonné ?

Ils sont rejetés. Lorsque le signal passé à util.throttle est abandonné, les appels en attente et futurs sont rejetés avec une AbortError, et la raison (reason) du signal est définie comme cause de l'erreur. Les invocations déjà démarrées continuent de s'exécuter. Contrairement aux rejets ERR_THROTTLED, la documentation n'indique pas que ces rejets AbortError sont marqués comme gérés : ajoutez donc un gestionnaire catch à chaque appel que vous n'attendez pas avec 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.