12k
All articles

Node.js bringt jetzt integriertes Debounce und Throttle mit

Node.js 26.10.0 und 24.22.0 bieten util.debounce und util.throttle. Erfahren Sie mehr über Optionen, Promises und Unterschiede zu lodash.

OpenReplay Team
OpenReplay Team
Node.js bringt jetzt integriertes Debounce und Throttle mit

Node.js 26.10.0 und 24.22.0 ergänzen das Modul node:util um util.debounce() und util.throttle(). Damit lassen sich Funktionen in Servercode debouncen und drosseln, ohne lodash oder throttle-debounce zu installieren.

Falls Sie schon wieder einen setTimeout/clearTimeout-Wrapper für einen File Watcher geschrieben oder lodash.debounce zu einem Service hinzugefügt haben, der sonst nichts aus lodash verwendet: Das ist nicht mehr nötig. Beide Helfer sind jetzt Teil der Runtime.

Dieser Artikel behandelt die Release-Linien, die beiden Signaturen und ihre Optionen, serverseitige Beispiele für beide Funktionen sowie die nötigen Anpassungen, wenn Sie lodash entfernen. Er setzt unsere Reihe über integrierte Node.js-APIs, die npm-Pakete ersetzen, fort.

Die wichtigsten Punkte

  • util.debounce() und util.throttle() sind ab Node.js 26.10.0 und 24.22.0 verfügbar. Beide stammen aus dem PR #65899 in nodejs/node.
  • Jeder Aufruf einer debouncten Funktion gibt ein Promise zurück. Alle Aufrufe innerhalb eines Debounce-Fensters werden mit dem Ergebnis eines einzigen Aufrufs von fn aufgelöst.
  • util.throttle(fn, limit, interval) lässt höchstens limit Aufrufe pro interval Millisekunden starten und stellt überzählige Aufrufe standardmäßig in eine Warteschlange, statt sie zu verwerfen.
  • Von util.throttle() verworfene Aufrufe werden mit ERR_THROTTLED abgelehnt, und Node markiert diese Rejections als behandelt.
  • Keine der beiden Funktionen bietet eine trailing- oder maxWait-Option. Von lodash portierter Code muss daher an die Optionen von Node angepasst werden.

Welche Node.js-Versionen util.debounce und util.throttle enthalten

Beide Funktionen wurden erstmals in Node.js 26.10.0 ausgeliefert, einem Current-Release vom 22. September 2026. Am 14. Oktober 2026 wurden sie nach Node.js 24.22.0 ‘Krypton’ LTS zurückportiert. Beide Änderungen stammen aus PR #65899 von James M Snell. Die util-Dokumentation gibt für beide Funktionen „Added in: v26.10.0“ an, die Release Notes von 24.22.0 führen sie jedoch unter den Notable Changes auf. In der 24.x-Linie ist 24.22.0 die Mindestversion, jeder spätere 24.x-Patch enthält sie also ebenfalls.

Wenn Sie eine Bibliothek veröffentlichen, die ältere Node-Versionen unterstützt, prüfen Sie vor der Verwendung, ob util.debounce existiert, oder behalten Sie einen Fallback bei.

Was ist der Unterschied zwischen Debounce und Throttle?

In Node.js führt util.debounce() die Funktion fn einmal aus, nachdem wait Millisekunden lang keine Aufrufe mehr eingegangen sind. util.throttle() begrenzt dagegen, wie viele Aufrufe von fn pro interval starten dürfen. Die grundlegende Funktionsweise von Grund auf erklärt der Artikel über Debouncing und Throttling für bessere Performance.

Wie funktioniert util.debounce in Node.js?

util.debounce(fn, wait[, options]) wartet, bis seit dem letzten Aufruf wait Millisekunden vergangen sind, und ruft dann fn auf. Jeder Aufruf innerhalb dieses Fensters gibt ein Promise zurück, das mit dem Ergebnis dieses einen Aufrufs aufgelöst wird. Die Referenz zu util.debounce nennt drei Optionen:

OptionStandardwertWirkung
leadingfalseBei true ruft der erste Aufruf in einem Fenster fn sofort auf
rejectOnCancelfalseBei true werden abgelöste Aufrufe mit einem AbortError abgelehnt
signalkeinerEin Abbruch storniert ausstehende Aufrufe und blockiert künftige. Bei einem bereits abgebrochenen Signal wirft debounce() einen Fehler

Die zurückgegebene Funktion bietet außerdem cancel([reason]), flush(), pending, pendingCount, ref() und unref().

Erfordert Node.js ≥ 24.22.0 oder ≥ 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 erhält nur die Argumente des letzten Aufrufs. Um alle Ereignisse eines Bursts zu erfassen, speichern Sie diese außerhalb von fn und lesen sie bei der Ausführung aus. Der folgende File Watcher nutzt dafür ein 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();
});

Ein Aufruf von unref() auf einer debouncten oder gedrosselten Funktion erlaubt es dem Prozess, sich zu beenden, obwohl ihr Timer noch aussteht. Das eignet sich für File Watcher und Log-Flusher, die das Herunterfahren nie blockieren sollten. unref() gibt die Funktion zurück, sodass Sie es direkt an den debounce()-Aufruf anhängen können. Beim Herunterfahren führt flush() den letzten Batch aus, damit er nicht verloren geht.

Anders als bei den ERR_THROTTLED-Rejections von Throttle markiert Debounce nicht jede Rejection als behandelt. Laut Node-Quellcode wird das Promise eines Aufrufs als behandelt markiert, sobald ein späterer Aufruf ihn ablöst – das Promise des letzten Aufrufs jedoch nicht. Lehnen cancel() oder ein abgebrochenes signal dieses Promise ab und fängt niemand den Fehler ab, kommt es im Prozess zu einer Unhandled Rejection. Hängen Sie bei Fire-and-Forget-Aufrufen wie im obigen Watcher-Callback ein .catch an.

Wie drosselt util.throttle Aufrufe?

util.throttle(fn, limit, interval[, options]) lässt höchstens limit Aufrufe von fn pro interval Millisekunden starten. limit muss größer als 0 sein. Standardmäßig werden Aufrufe über dem Limit in der Reihenfolge ihres Eingangs in eine Warteschlange gestellt, statt verworfen zu werden. Die Referenz zu util.throttle definiert folgende Optionen:

  • concurrency (Standard Infinity): die maximale Anzahl gleichzeitig laufender fn-Aufrufe. Ein Slot wird freigegeben, sobald der Rückgabewert von fn settled ist.
  • maxPending (Standard Infinity): die Obergrenze der Warteschlange. Im Drop-Modus hat sie keine Wirkung.
  • overflow (standardmäßig 'queue', alternativ 'drop').
  • strict (Standard false).
  • signal: Bei einem Abbruch werden ausstehende und künftige Aufrufe mit einem AbortError abgelehnt.

Die Ratenkapazität wird verbraucht, wenn fn startet, nicht wenn ein Aufruf in die Warteschlange aufgenommen wird. Die Funktion stellt außerdem cancel([reason]), hasImmediateCapacity(), pending, pendingCount, activeCount, ref() und unref() bereit.

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

Standardmäßig zählt util.throttle() Aufrufe in festen Zeitfenstern (Fixed Windows). Dadurch können zwei Gruppen von Aufrufen an einer Fenstergrenze kurz hintereinander starten. Mit strict: true wird das Limit stattdessen über jedes gleitende Intervall (Rolling Interval) hinweg durchgesetzt.

Um eine Funktion höchstens einmal pro Sekunde auszuführen und überzählige Aufrufe zu verwerfen, statt sie einzureihen, verwenden Sie 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

Von util.throttle() verworfene Aufrufe werden mit einem ERR_THROTTLED-Fehler abgelehnt. Ein Aufruf wird verworfen, wenn overflow auf 'drop' steht oder die maxPending-Warteschlange voll ist. Node markiert diese Promises als behandelt, und verworfene Aufrufe verbrauchen weder Raten- noch Concurrency-Kapazität. cancel() leert die Warteschlange und setzt das Zeitfenster zurück, stoppt aber keine bereits gestarteten Aufrufe.

Wie ersetzt man lodash.debounce und lodash.throttle?

util.debounce() und util.throttle() können lodash.debounce und lodash.throttle in den meisten Servercodes ersetzen, folgen aber nicht demselben Modell wie lodash. Der Throttle von Node begrenzt, wie viele Aufrufe pro Intervall starten dürfen, und reiht die übrigen ein. Der Throttle von lodash erlaubt höchstens einen Aufruf pro wait. Portieren Sie Ihren Code ausschließlich anhand der Optionen von node:util:

AspektVerhalten von node:utilWas zu ändern ist
RückgabewertJeder Aufruf gibt ein Promise zurückDas Promise mit await oder .catch behandeln
Edge-OptionenNur leading; es gibt keine trailing- oder maxWait-OptionenDiese Keys entfernen und maxWait-Logik neu denken, etwa mit einem Throttle
Throttle-ModellErlaubt limit Starts pro interval und reiht überzählige Aufrufe einFür das Überspringen überzähliger Aufrufe ein limit von 1 plus overflow: 'drop' verwenden
Manuelle Steuerungcancel([reason]) und flush()Direkt auf dem Wrapper aufrufen
thisInnerhalb von fn ist this der WrapperMethoden vorher binden
// 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();

Wann Sie lodash oder einen eigenen Helfer behalten sollten

In zwei Fällen sollten Sie lodash, throttle-debounce oder einen selbst geschriebenen Helfer statt node:util beibehalten:

  • Browser-Code. node:util ist ein serverseitiges Modul. Frontend-Bundles benötigen weiterhin einen eigenen Helfer; siehe Debounce-Strategien in React.
  • Node-Versionen vor 24.22.0. Dazu gehören frühere 24.x-Patches und alle älteren Release-Linien. Projekte, die diese Versionen unterstützen müssen, sollten die Abhängigkeit behalten oder eine Feature-Prüfung ergänzen.

Fazit

Ab Node.js 26.10.0 und 24.22.0 sind Debouncing und Rate Limiting Teil von node:util. Beide Funktionen geben Promises zurück und bringen cancel() und unref() direkt mit; die debouncte Funktion bietet zusätzlich flush(). Als nächsten Schritt sollten Sie herausfinden, wo Ihr Servercode lodash.debounce oder lodash.throttle verwendet, und einen File Watcher oder einen ausgehenden Client auf die integrierte Variante umstellen. Binden Sie Methoden, bevor Sie sie wrappen, und hängen Sie an jeden Aufruf, den Sie nicht mit await abwarten, ein .catch an.

FAQs

Führt util.debounce mit leading: true die Funktion zweimal aus?

Nur wenn während des Fensters weitere Aufrufe eingehen. Ist leading auf true gesetzt, ruft der erste Aufruf in einem Debounce-Fenster fn sofort auf. Ein zweiter, nachlaufender Aufruf erfolgt nur, wenn die debouncte Funktion vor Ablauf der Wartezeit erneut aufgerufen wurde. Ein einzelner, isolierter Aufruf führt fn daher genau einmal aus, an der führenden Flanke, und es gibt keine trailing-Option zum Abschalten.

Wartet util.debounce, bis eine asynchrone Funktion abgeschlossen ist, bevor ein neues Fenster beginnt?

Nein. Nach einem nachlaufenden Aufruf kann util.debounce ein neues Debounce-Fenster öffnen, auch wenn das von fn zurückgegebene Promise noch nicht settled ist. Zwei Durchläufe einer langsamen asynchronen fn können sich also überschneiden. Um überlappende Durchläufe zu verhindern, verwenden Sie util.throttle mit concurrency auf 1. Dessen Concurrency-Slot wird erst freigegeben, wenn der Rückgabewert von fn settled ist.

Wie prüfe ich die Verfügbarkeit von util.debounce, ohne dass der Code auf älteren Node.js-Versionen bricht?

Importieren Sie das gesamte Modul und prüfen Sie die Eigenschaft: import util from 'node:util', dann vor der Verwendung typeof util.debounce === 'function' testen und bei Fehlen auf einen Userland-Helfer zurückgreifen. In ESM schlägt ein Named Import eines nicht existierenden Exports beim Linken des Moduls mit einem SyntaxError fehl, sodass der Code die Prüfung gar nicht erst ausführen kann.

Was passiert mit eingereihten util.throttle-Aufrufen, wenn das zugehörige AbortSignal abgebrochen wird?

Sie werden abgelehnt. Wird das an util.throttle übergebene Signal abgebrochen, werden ausstehende und künftige Aufrufe mit einem AbortError abgelehnt, wobei der reason des Signals als cause des Fehlers gesetzt wird. Bereits gestartete Aufrufe laufen weiter. Anders als bei ERR_THROTTLED-Rejections gibt die Dokumentation nicht an, dass diese AbortError-Rejections als behandelt markiert werden. Hängen Sie daher an jeden Aufruf, den Sie nicht mit await abwarten, einen catch-Handler an.

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.