Debouncing und Throttling in JavaScript: Events unter Kontrolle bringen
Manche Events feuern hunderte Male pro Sekunde. Mit Debouncing und Throttling zähmst du sie und sorgst für flüssige, performante Web-Anwendungen.
Stell dir vor, du baust ein Suchfeld, das bei jeder Eingabe eine Anfrage an den Server schickt. Tippt jemand „JavaScript“, löst du damit zehn Requests aus – obwohl dich nur der letzte interessiert. Genau solche Situationen entstehen ständig: bei scroll, resize, mousemove oder input feuern Browser-Events teils hunderte Male pro Sekunde. Debouncing und Throttling sind zwei Techniken, mit denen du diese Flut kontrollierst. In diesem Beitrag lernst du, was sie unterscheidet und wie du beide selbst schreibst.
Das Problem: zu viele Aufrufe
Schauen wir uns zuerst an, wie oft ein Event tatsächlich feuert. Der folgende Code hängt sich an das scroll-Event und zählt die Aufrufe:
let counter = 0;
window.addEventListener('scroll', () => {
counter++;
console.log(`Scroll-Event Nr. ${counter}`);
});
Scrollst du nur ein paar Sekunden, stehen im Log schnell mehrere hundert Einträge. Wenn in diesem Handler eine teure Berechnung oder ein Netzwerk-Request steckt, ruckelt deine Seite – der Main-Thread kommt nicht hinterher. Beide Techniken lösen das, aber auf unterschiedliche Weise.
Debouncing: erst warten, dann handeln
Beim Debouncing wird eine Funktion erst dann ausgeführt, wenn seit dem letzten Aufruf eine bestimmte Zeit vergangen ist. Solange neue Events eintreffen, wird der Timer immer wieder zurückgesetzt. Perfekt fürs Suchfeld: Du reagierst erst, wenn der Nutzer eine kurze Pause macht.
function debounce(fn, delay) {
let timerId;
return function (...args) {
clearTimeout(timerId);
timerId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
Der Trick steckt im Closure: Die zurückgegebene Funktion merkt sich timerId zwischen den Aufrufen. Jeder neue Aufruf löscht den alten Timer mit clearTimeout und startet einen frischen. Erst wenn delay Millisekunden lang nichts passiert, läuft fn wirklich. So setzt du es beim Suchfeld ein:
const input = document.querySelector('#suche');
const sucheAusfuehren = debounce((event) => {
console.log('Suche nach:', event.target.value);
// hier käme dein fetch(...) hin
}, 400);
input.addEventListener('input', sucheAusfuehren);
Egal wie schnell jemand tippt – die Suche startet erst 400 ms nach dem letzten Tastendruck. Aus zehn potenziellen Requests wird genau einer.
Throttling: in festen Abständen handeln
Throttling verfolgt ein anderes Ziel: Die Funktion soll höchstens einmal pro Zeitfenster laufen, egal wie oft das Event feuert. Das eignet sich für scroll- oder resize-Handler, bei denen du regelmäßige Updates willst, aber nicht bei jedem einzelnen Pixel.
function throttle(fn, limit) {
let wartend = false;
return function (...args) {
if (!wartend) {
fn.apply(this, args);
wartend = true;
setTimeout(() => {
wartend = false;
}, limit);
}
};
}
Hier merkt sich der Closure das Flag wartend. Der erste Aufruf läuft sofort durch und sperrt danach für limit Millisekunden alle weiteren Aufrufe. Erst wenn das Zeitfenster abgelaufen ist, ist die Funktion wieder frei. Ein typischer Einsatz beim Scroll-Fortschritt:
const aktualisiereFortschritt = throttle(() => {
const scrollTop = window.scrollY;
const hoehe = document.body.scrollHeight - window.innerHeight;
const prozent = Math.round((scrollTop / hoehe) * 100);
console.log(`Gelesen: ${prozent}%`);
}, 200);
window.addEventListener('scroll', aktualisiereFortschritt);
Statt hunderter Aufrufe pro Sekunde bekommst du jetzt maximal fünf – ausreichend flüssig fürs Auge und deutlich schonender für den Browser.
Debounce oder Throttle – wann was?
Die beiden Techniken sehen ähnlich aus, verhalten sich aber grundverschieden. Als Faustregel hilft dir diese Übersicht:
- Debounce nutzt du, wenn nur das Endergebnis zählt: Sucheingaben, das Validieren eines Formularfelds oder das Speichern eines Entwurfs, nachdem der Nutzer aufgehört hat zu tippen.
- Throttle nutzt du, wenn du regelmäßige Zwischenstände brauchst: Scroll-Positionen, Fenstergröße beim Resizen, das Verfolgen der Mausbewegung oder ein Endlos-Scroll.
Kurz gesagt: Debounce fragt „Ist der Nutzer fertig?“, Throttle fragt „Ist genug Zeit vergangen?“. Beide bauen auf denselben Grundbausteinen auf – setTimeout und einem Closure, der Zustand zwischen den Aufrufen festhält.
Aufräumen nicht vergessen
Ein Detail, das Einsteiger oft übersehen: Wenn du deine Handler dynamisch hinzufügst (etwa in einem Framework-Component), solltest du sie beim Verlassen wieder entfernen. Sonst laufen alte Timer weiter und du sammelst Speicherlecks an. Wichtig ist dabei, dass du dieselbe Funktionsreferenz übergibst:
const handler = debounce(() => console.log('resize!'), 300);
window.addEventListener('resize', handler);
// später, beim Aufräumen:
window.removeEventListener('resize', handler);
Deshalb speicherst du das Ergebnis von debounce(...) immer in einer Variablen, statt es direkt inline zu übergeben – nur so hast du später eine Referenz zum Entfernen.
Fazit
Debouncing und Throttling sind kleine Helfer mit großer Wirkung. Mit wenigen Zeilen zähmst du Events, die sonst deine Anwendung ausbremsen, und sorgst für spürbar flüssigere Oberflächen. Merke dir die Kernidee: Debounce wartet auf Ruhe, Throttle begrenzt die Frequenz. Beide bauen auf Closures und setTimeout auf – wenn du diese Konzepte verstanden hast, kannst du die Funktionen jederzeit selbst schreiben, statt blind auf eine Bibliothek zu setzen. Probier es im nächsten Projekt aus: Bau ein Suchfeld mit Debounce und einen Scroll-Fortschrittsbalken mit Throttle. So sitzt der Unterschied garantiert.