Ein Suchfeld, das bei jedem Tastendruck eine Anfrage auslöst, schickt für “redis” fünf Anfragen, von denen vier veraltet sind, bevor die Antwort zurückkommt. Ein scroll-Handler, der das Layout neu berechnet, läuft hunderte Mal pro Sekunde und lässt die Seite stottern. Beide Fälle haben dasselbe Problem: Das DOM-Event feuert viel öfter, als der Handler laufen muss. Debounce und Throttle lösen das auf zwei unterschiedliche Arten.
Was Debounce macht und was Throttle macht
Debounce wartet, bis das Event für eine festgelegte Zeit nicht mehr feuert, und läuft dann einmal. Jeder neue Aufruf setzt die Uhr zurück. Hört das Event nie auf, läuft die Funktion nie. Ein Suchfeld ist der klassische Fall: warten, bis der Nutzer beim Tippen pausiert, und dann eine Anfrage für das zuletzt Eingegebene schicken.
Throttle begrenzt, wie oft der Aufruf laufen darf. Während eines Schubs läuft er weiter, höchstens einmal pro Intervall. Nutzen Sie es für scroll, wo die Seite sich weiter aktualisieren soll, während der Nutzer scrollt, nur nicht bei jedem einzelnen Event.
| Debounce | Throttle | |
|---|---|---|
| Läuft während des Schubs | Erst nach dessen Ende | Ja, höchstens einmal pro Intervall |
| Was Sie erhalten | Den letzten Aufruf des Schubs | Eine gleichmäßige Stichprobe über den ganzen Schub |
| Typischer Einsatz | Suchfeld, Formularvalidierung, Speichern beim Tippen | scroll, resize, mousemove, Drag |
| Wenn der Schub nie endet | Läuft nie | Läuft weiter nach Zeitplan |
Beide sind Wrapper. Sie übergeben Ihren Callback und eine Verzögerung und erhalten dafür eine Funktion mit derselben Signatur, die Sie statt der ursprünglichen an das Event hängen.
Eine Debounce-Funktion schreiben
| |
Jeder Aufruf bricht den wartenden setTimeout ab und plant einen neuen, sodass nur der letzte Aufruf des Schubs lange genug lebt, um auszulösen. Das Timing folgt den setTimeout-Regeln aus wie die Event-Loop in JavaScript Callbacks einplant: Der Callback läuft nie vor delay, und kann später laufen, wenn der Call-Stack beim Ablauf des Timers belegt ist.
Angeschlossen an ein Suchfeld:
| |
Tippen Sie “redis” im normalen Tempo, zeigt die Konsole eine einzige Zeile, searching for: redis, etwa 300ms nach dem letzten Tastendruck. Nicht fünf Zeilen für fünf Buchstaben.
Eine Throttle-Funktion schreiben
| |
Das ist die Leading-Edge-Version. Der erste Aufruf in einem Fenster läuft sofort; alles danach wird verworfen, bis interval Millisekunden vergangen sind.
| |
Scrollen Sie zwei Sekunden lang durchgehend, erhalten Sie etwa zehn gleichmäßig verteilte Log-Zeilen statt der hunderten scroll-Events, die der Browser tatsächlich auslöst.
Lodash-Optionen für Debounce und Throttle: leading, trailing, maxWait
Jede der oben handgeschriebenen Funktionen legt genau eine Variante fest. Debounce löst nur auf dem Trailing Edge aus, nach der ruhigen Phase. Throttle löst nur auf dem Leading Edge aus, im Moment, in dem ein Fenster öffnet, sodass der letzte Aufruf eines Schubs verloren geht, wenn er mitten im Fenster landet. Das zählt bei scroll: Die letzte Position, an der der Nutzer gestoppt hat, wird vielleicht nie verarbeitet.
Lodashs _.debounce(fn, wait, options) und _.throttle(fn, wait, options) machen beide Kanten über die Booleans leading und trailing konfigurierbar, und die Standardwerte unterscheiden sich. Debounce hat standardmäßig leading: false, trailing: true: warten, dann auslösen. Throttle hat standardmäßig beide auf true: sofort auslösen, dann noch einmal am Ende eines Schubs, der länger als ein Intervall gedauert hat. Intern ist Lodashs Throttle sein eigenes Debounce mit maxWait auf das Intervall gesetzt, die beiden liegen also näher beieinander, als die Namen vermuten lassen.
maxWait ist eine Debounce-Option, die begrenzt, wie lange der erste Aufruf verschoben werden kann. Ohne sie löst jemand, der mit Pausen kürzer als die Verzögerung gleichmäßig tippt, den Handler nie aus, selbst nach zehn Sekunden Aktivität nicht. Mit maxWait: 2000 löst er mindestens alle zwei Sekunden aus.
Nur-Leading-Debounce deckt den Fall doppelter Absendungen ab. _.debounce(sendMail, 300, { leading: true, trailing: false }) löst beim ersten Klick aus und ignoriert weitere Klicks, bis 300ms ohne jeden Klick vergangen sind. Der erste Klick zählt; die übrigen werden verworfen, nicht verzögert.
Welche Events Debounce brauchen und welche Throttle
| Event | Wahl | Warum |
|---|---|---|
| Suche beim Tippen | Debounce | Nur die letzte Anfrage zählt; frühere Tastendrücke erzeugen Anfragen, die Sie wegwerfen würden |
resize des Fensters | Debounce oder Throttle | Debounce, wenn nur die Endgröße zählt (Layout einmal neu berechnen); Throttle, wenn die UI das Resizing sichtbar mitverfolgen soll |
scroll | Throttle | Die Seite soll während des gesamten Scrollens reagieren, nicht nur, nachdem der Nutzer stoppt |
mousemove (Drag, Tooltip, das folgt) | Throttle, über requestAnimationFrame | Updates schneller als die Bildwiederholrate des Bildschirms sind verschwendete Arbeit |
| Button-Klick (Doppel-Absenden verhindern) | Nur-Leading-Debounce oder Deaktivieren beim Klick | Der erste Klick soll zählen; die übrigen sollen ignoriert, nicht verzögert werden |
| Autosave bei jedem Tastendruck | Debounce mit maxWait | Nach einer Pause speichern, aber ein Speichern während langem durchgehendem Tippen garantieren |
Steuert der Handler etwas Visuelles (einen Drag, einen dem Cursor folgenden Tooltip, eine an den Scroll gekoppelte Animation), ersetzen Sie Date.now durch requestAnimationFrame:
| |
Die Aufruffrequenz folgt jetzt der Paint-Rate des Browsers statt einer frei gewählten Millisekundenzahl. Das Update läuft einmal pro Frame, direkt bevor der Frame gezeichnet wird, kann also nicht öfter laufen, als irgendjemand sehen kann, und driftet nicht vom Rendering ab, wenn der Main-Thread unter Last steht.
Häufige Fehler mit Debounce und Throttle
Die debounced Funktion innerhalb des Handlers erzeugen. In React ruft onChange={(e) => debounce(handleSearch, 300)(e.target.value)} bei jedem Tastendruck debounce() auf, und jeder Aufruf liefert einen neuen Wrapper mit eigenem, leerem timer. Es gibt nie einen vorherigen Timer, den man abbrechen könnte, also löst jeder Tastendruck trotzdem eine Anfrage aus, nur 300ms später. Erzeugen Sie die debounced Funktion nur einmal (useMemo, useRef, oder Modul-Scope außerhalb einer Komponente) und rufen Sie stets diese eine Instanz auf.
Beim Unmount nicht abbrechen. Ein debounced Autosave, der auslöst, nachdem der Nutzer weiternavigiert ist, schreibt in eine Komponente, die nicht mehr existiert, oder speichert aus einer veralteten Closure. Lodashs debounced und throttled Funktionen haben bereits eine .cancel()-Methode; fügen Sie dem handgeschriebenen Debounce eine hinzu, die clearTimeout(timer) aufruft. Rufen Sie sie auf, wenn die Komponente unmountet oder die Seite entladen wird.
Mit echten Timern testen. Einen Debounce mit await new Promise(r => setTimeout(r, 300)) abzuwarten funktioniert, aber fügen Sie das zu jedem Timing-Test hinzu, und die Suite wird langsam genug, dass niemand sie mehr lokal laufen lässt. Nutzen Sie stattdessen Fake-Timer: vi.useFakeTimers() in Vitest oder jest.useFakeTimers() in Jest, und schieben Sie die Uhr mit vi.advanceTimersByTime(300) vor. Ein Fake-Timer ist ein Test-Double für die Uhr, nicht für Ihren Code. Genau diese Unterscheidung erklärt Unit-Tests vs Integrationstests vs End-to-End-Tests im Detail.
Debounce auf etwas anwenden, das live wirken muss. Ein Fortschrittsbalken oder ein Drag-Griff, der sich erst bewegt, wenn der Nutzer stoppt, wirkt kaputt. Wenn die Anforderung “flüssig aktualisieren, während das weiterläuft” lautet, brauchen Sie Throttle.
Debounce oder Throttle als Standard?
Verwenden Sie Debounce, wenn nur der Endzustand zählt: Suchfelder, Autosave, ein Feld validieren, sobald der Nutzer aufhört zu tippen. Verwenden Sie Throttle, wenn der Nutzer erwartet, während etwas Fortlaufendem Fortschritt zu sehen, wie beim Scrollen, Resizing oder Drag, und wechseln Sie zur requestAnimationFrame-Version, wenn das Ergebnis ein visuelles Update ist.
Importieren Sie in Produktionscode debounce und throttle aus einer gepflegten Bibliothek (Lodash, oder einem Hook wie use-debounce in React), statt die Versionen oben zu verschicken. Sie existieren, um den Mechanismus zu zeigen. Die Bibliotheksversionen behandeln Abbruch, maxWait, Trailing-Aufrufe und this-Binding bereits selbst, genau dort, wo eine handgeschriebene Kopie meist bricht.