W3docs

JavaScript Debounce und Throttle

Lerne, wie man Funktionen in JavaScript mit Debounce und Throttle ratenbegrenzt — was jede macht, wie man sie implementiert und wann man welche für Input-, Scroll- und Resize-Handler einsetzt.

Manche Ereignisse werden weit öfter ausgelöst, als man sinnvoll darauf reagieren kann. Das Tippen in ein Suchfeld löst bei jedem Tastendruck ein input-Ereignis aus; das Scrollen einer Seite kann Hunderte von scroll-Ereignissen pro Sekunde erzeugen; resize und mousemove sind genauso geschwätzig. Wenn jedes Ereignis aufwändige Arbeit auslöst — eine Netzwerkanfrage, eine Layout-Berechnung, ein Re-Render — ruckelt die App. Debounce und Throttle sind zwei kleine Wrapper, die begrenzen, wie oft eine Funktion ausgeführt wird, und so die Reaktionsfähigkeit hoch halten, ohne das Verhalten der Funktion zu ändern.

Beide sind klassische Decorator-Muster: Sie nehmen eine Funktion und geben eine neue Funktion mit demselben Verhalten plus einer Ratenbegrenzungsregel zurück. Sie bauen auf Closures auf, um sich den Zustand zwischen Aufrufen zu merken, und auf Timern wie setTimeout, um die Ausführung zu verzögern oder zu steuern.

Die Grundidee

Die beiden Techniken beantworten dieselbe Frage — „wie oft soll das ausgeführt werden?" — auf entgegengesetzte Weise:

  • Debounce wartet auf eine Pause. Es verschiebt den Aufruf, bis N Millisekunden seit der letzten Ausführung vergangen sind. Kommen ständig neue Aufrufe, wird der Timer immer wieder zurückgesetzt und die Funktion wird nie ausgelöst. Merkhilfe: „Warte auf Stille."
  • Throttle erzwingt einen gleichmäßigen Takt. Es lässt die Funktion höchstens einmal pro N Millisekunden ausführen, egal wie oft sie zwischendurch aufgerufen wird. Merkhilfe: „Regelmäßiger Herzschlag."
AspektDebounceThrottle
Wird ausgelöst wennAktivität stoppt für N msHöchstens einmal alle N ms
Während eines BurstsNichts wird ausgeführt bis der Burst endetLäuft nach festem Zeitplan
Denkmodell„Warte auf Stille"„Gleichmäßiger Takt"
Geeignet fürSuche beim Tippen, Autosave, Resize abgeschlossenScroll-Tracking, Drag, Infinite Scroll

Debounce

Eine debounced Funktion löscht bei jedem Aufruf den ausstehenden Timer und plant einen neuen. Erst wenn die Aufrufe schließlich für delay Millisekunden ausbleiben, wird die umhüllte Funktion tatsächlich ausgeführt.

function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

Zwei Details machen das robust. Der Wrapper sammelt alle Argumente mit Rest-Parametern (...args) und leitet sie weiter, sodass die umhüllte Funktion genau das erhält, was der Aufrufer übergeben hat. Und er ruft fn mit fn.apply(this, args) auf, damit das ursprüngliche this erhalten bleibt — wichtig, wenn die debounced Funktion eine Methode eines Objekts ist. (Siehe call und apply und Funktionsbindung, warum die Weiterleitung von this wichtig ist.)

Hier ist es in Aktion. Das mehrfache Aufrufen der umhüllten Funktion löst nur einen einzigen echten Lauf aus, nachdem die Aktivität zur Ruhe kommt:

javascript— editable

Da jeder Tastendruck die Uhr zurücksetzt, ist debounce ideal, wenn man nach dem Abschluss einer Nutzereingabe reagieren möchte: Suche beim Tippen, automatisches Speichern eines Entwurfs, Validierung eines Felds nach dem Tippen oder Neuberechnung eines Layouts erst wenn eine Fenstergrößenänderung abgeschlossen ist.

Throttle

Eine throttled Funktion wird sofort ausgeführt und ignoriert dann weitere Aufrufe, bis eine Abkühlzeit verstrichen ist. Das garantiert eine maximale Häufigkeit, anstatt auf eine Pause zu warten.

function throttle(fn, limit) {
  let inThrottle = false;
  return function (...args) {
    if (!inThrottle) {
      fn.apply(this, args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}

Das inThrottle-Flag, das in der Closure gespeichert wird, fungiert als Tor. Der erste Aufruf passiert und das Tor schließt sich; alle Aufrufe während der Abkühlzeit werden verworfen; wenn der Timer ausläuft, öffnet sich das Tor für den nächsten Aufruf wieder.

javascript— editable

Throttle eignet sich für alles, was kontinuierlich strömt und bei dem man regelmäßige Aktualisierungen statt jeder einzelnen möchte: Scroll-Position verfolgen, mousemove während eines Drags verarbeiten, mehr Inhalte beim Infinite Scroll laden oder begrenzen, wie oft man eine ratenbegrenzte API aufruft.

Leading vs. Trailing Edge

Bei beiden Wrappern gibt es eine subtile Designentscheidung: Soll die Funktion an der Leading Edge (dem ersten Aufruf, sofort) oder an der Trailing Edge (nach der Verzögerung/Abkühlzeit) ausgelöst werden?

  • Das obige debounce ist trailing: Nichts passiert, bis die Aktivität aufhört. Ein leading-edge Debounce würde beim ersten Aufruf ausgeführt werden und den Rest ignorieren.
  • Das obige throttle ist leading: Es wird sofort ausgelöst und dann gesteuert. Ein trailing-edge Throttle würde am Ende des Fensters noch einmal ausgeführt, um den finalen Wert zu erfassen.

Diese Edge-Verhaltensweisen sind in der Praxis wichtig — ein trailing Throttle beim Scrollen stellt zum Beispiel sicher, dass die finale Scrollposition nicht verpasst wird, wenn der Nutzer aufhört zu scrollen.

Info

Für Produktionscode sollte man eine bewährte Implementierung wie Lodash's _.debounce und _.throttle bevorzugen. Diese behandeln Leading und Trailing Edges, bieten eine cancel()/flush()-API und eine maxWait-Option (damit eine debounced Funktion auch bei kontinuierlicher Aktivität irgendwann ausgeführt wird). Die obigen Kernversionen zu verstehen ist wesentlich, aber man muss selten eine eigene implementieren.

Ein echtes DOM-Beispiel

Debounce an ein Sucheingabefeld zu koppeln ist der klassische Anwendungsfall. Man fügt einen Listener hinzu (siehe Ereignisbehandlung im DOM) und lässt den Wrapper entscheiden, wann die eigentliche Arbeit ausgeführt wird:

const input = document.querySelector('#search');

function search(event) {
  console.log('Querying API for:', event.target.value);
  // fetch(`/api/search?q=${event.target.value}`) ...
}

const debouncedSearch = debounce(search, 400);

input.addEventListener('input', debouncedSearch);

Jetzt wird die Netzwerkanfrage erst ausgeführt, wenn der Nutzer 400 ms pausiert, statt bei jedem Tastendruck — ein Suchfeld, das zuvor ein Dutzend Anfragen für hello abgeschickt hat, schickt nun eine. Beachte, dass der Listener das DOM-event-Objekt erhält, und da unser Wrapper alle Argumente weiterleitet, erhält search es unverändert.

Warnung

Timer und Listener halten Referenzen, also sollten sie aufgeräumt werden, wenn sie nicht mehr benötigt werden. In einer Single-Page-App oder Komponente sollte man den Listener beim Abbau entfernen (zum Beispiel beim Unmounten der Komponente) und ausstehende Timer löschen, um Memory Leaks und Callbacks zu vermeiden, die auf nicht mehr vorhandene Elemente ausgelöst werden:

input.removeEventListener('input', debouncedSearch);

Ein Produktions-debounce bietet typischerweise auch eine cancel()-Methode, die clearTimeout für einen aufruft.

Die Wahl zwischen beiden

Wenn man unsicher ist, welches man verwenden soll, sollte man sich fragen, worauf es ankommt:

  • Ist nur der finale Zustand nach einem Aktivitätsburst relevant (der abgeschlossene Suchbegriff, die festgelegte Fenstergröße)? Verwende Debounce.
  • Möchte man kontinuierliches, regelmäßiges Feedback während der Aktivität (Scroll-Fortschritt, die Position eines gezogenen Elements)? Verwende Throttle.

Beide sind leichtgewichtig, Framework-agnostisch und bauen direkt auf Closures und Timern auf — denselben Grundlagen wie Pfeilfunktionen, die this erfassen und den Scheduling-Werkzeugen, die man bereits kennengelernt hat.

Teste dein Wissen

Übung
Welche Technik führt die Funktion höchstens einmal pro festem Zeitintervall aus, egal wie oft sie aufgerufen wird?
Welche Technik führt die Funktion höchstens einmal pro festem Zeitintervall aus, egal wie oft sie aufgerufen wird?
Übung
Du möchtest eine Suchanfrage erst senden, wenn der Nutzer aufgehört hat zu tippen. Welches ist das richtige Werkzeug?
Du möchtest eine Suchanfrage erst senden, wenn der Nutzer aufgehört hat zu tippen. Welches ist das richtige Werkzeug?
Übung
Warum rufen die Beispiel-Debounce- und Throttle-Wrapper die ursprüngliche Funktion mit `fn.apply(this, args)` statt einfach `fn(args)` auf?
Warum rufen die Beispiel-Debounce- und Throttle-Wrapper die ursprüngliche Funktion mit `fn.apply(this, args)` statt einfach `fn(args)` auf?
Was this page helpful?