debounced() erzeugt eine „verzögerte“ Version eines Signals: Der Wert wird erst aktualisiert, nachdem die Quelle eine Weile unverändert geblieben ist. Das ist der klassische Fall der Suchbox: Du willst keinen Request bei jedem Tastendruck, sondern erst, wenn die Nutzerin oder der Nutzer aufgehört hat zu tippen.
debounced ist als experimentell markiert (@experimental 22.0): Die API kann sich ändern. Du kannst sie bedenkenlos nutzen, solltest sie aber isoliert halten (zum Beispiel in einem Store oder einer Such-Komponente), damit eine mögliche Änderung leicht umzusetzen ist.
Beispiel: Suche mit Debounce + httpResource
import { httpResource } from '@angular/common/http';
import { Component, debounced, signal } from '@angular/core';
@Component({
selector: 'app-user-search',
template: `
<input [value]="query()" (input)="query.set($any($event.target).value)" />
@if (search.isLoading()) { <span>warte, bis du fertig getippt hast…</span> }
@for (u of users.value().items; track u.id) {
<p>{{ u.name }}</p>
}
`,
})
export class UserSearch {
readonly query = signal('');
// debounced() liefert eine Resource: Der Wert wird 300 ms nach der LETZTEN Änderung aktualisiert.
readonly search = debounced(this.query, 300);
readonly users = httpResource<PageDto<UserDto>>(
() => `/api/users?q=${encodeURIComponent(this.search.value())}`,
{ defaultValue: { items: [], total: 0 } },
);
}∅∅- Ana Popescu · Cluj-Napoca
- Mihai Ionescu · București
- Ioana Dumitru · Iași
- Radu Stan · Timișoara
- Elena Marin · Brașov
- Andrei Georgescu · Cluj-Napoca
- Maria Constantin · Constanța
- Vlad Petrescu · Sibiu
- Cristina Pop · Oradea
- Bogdan Rusu · Iași
Debounce, Throttle oder keins von beiden
Beide verringern die Zahl der Requests, auf unterschiedliche Weise. Debounce wartet, bis der Nutzer eine Pause macht, und sendet nur den letzten Wert. Throttle sendet höchstens einmal alle X Millisekunden, solange die Events weiterkommen — der Bildschirm aktualisiert sich also auch während einer langen Salve.
Jeder Punkt ist ein Request; die Zahl zeigt, wie viele es waren. Fahr mit der Maus über einen Punkt, um den gesendeten Wert zu sehen.
Für die Suche beim Tippen ist Debounce fast immer richtig: Niemand braucht Ergebnisse für „ang“, wenn er gleich „angular“ tippt. Throttle passt zu kontinuierlichen Signalen, bei denen der neueste Zustand während der Bewegung zählt: Scrollposition, die Vorschau eines Reglers, Größenänderung, ein in Echtzeit geteilter Cursor. Die Debounce-Spur im Lab nutzt das echte debounced() mit einer Wartefunktion, die den Regler liest — eine neue Wartezeit gilt also ab dem nächsten Tastendruck.
import { debounced } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { fromEvent, map, throttleTime } from 'rxjs';
// Debounce: auf eine Pause warten, dann der letzte Wert (Suche beim Tippen)
readonly term = debounced(this.query, 300);
// Throttle: höchstens einmal pro Fenster, erster und letzter Wert inklusive (Scroll, Resize)
readonly scrollY = toSignal(
fromEvent(window, 'scroll').pipe(
throttleTime(100, undefined, { leading: true, trailing: true }),
map(() => window.scrollY),
),
{ initialValue: 0 },
);API
Das Ergebnis ist kein einfaches Signal, sondern eine Resource: Du bekommst value() und außerdem isLoading() — nützlich, um „Ich warte, bis du fertig getippt hast“ anzuzeigen.
const d = debounced(source, wait, options?);
// source: jede reaktive Funktion — ein Signal, ein computed, ein input()
// wait: eine Anzahl ms ODER (value, lastSnapshot) => Promise<void> | void
// options: { injector?, equal? }
// Ergebnis: Resource<T> → d.value(), d.status(), d.isLoading()
// Beispiel: keine Verzögerung, wenn das Feld geleert wird, sonst 400 ms
const term = debounced(this.query, (value) =>
value === '' ? undefined : new Promise<void>((resolve) => setTimeout(resolve, 400)),
);Früher und heute
// Heute (v22, experimentell)
readonly query = signal('');
readonly debouncedQuery = debounced(this.query, 300); // .value()// Früher (RxJS): drei Konvertierungen für ein einziges Debounce
readonly query = signal('');
readonly debouncedQuery = toSignal(
toObservable(this.query).pipe(debounceTime(300), distinctUntilChanged()),
{ initialValue: '' },
);Die RxJS-Variante ist nicht falsch — hast du bereits einen RxJS-Stream (mit switchMap, retry usw.), bleib dabei. Aber für „Ich will nur einen verzögerten Wert“ ist debounced viel einfacher.
In Formularen: debounce() aus Signal Forms
Für Felder eines Signal Forms brauchst du debounced() nicht: Es gibt die debounce()-Regel im Schema, und auch asynchrone Validatoren haben eine Debounce-Option (v22). Siehe Signal Forms — fortgeschritten.
// Signal Forms hat eine eigene debounce()-Regel für Felder:
import { debounce, form, validateHttp } from '@angular/forms/signals';
readonly signup = form(this.model, (path) => {
debounce(path.username, 300); // das Modell wird 300 ms nach dem Tippen aktualisiert
validateHttp(path.username, {
request: ({ value }) => `/api/username-available?username=${value()}`,
onSuccess: (res: { available: boolean }) =>
res.available ? undefined : { kind: 'taken', message: 'Nutzername vergeben' },
onError: () => ({ kind: 'network', message: 'Konnte nicht geprüft werden' }),
});
});Wann ja, wann nein
- Suche / Autocomplete.
- Filter, die Requests auslösen (ein Preis-Slider, ein numerisches Input).
- Autosave, nachdem die Nutzerin oder der Nutzer aufgehört hat zu tippen.
- Aufwendige Berechnungen, die aus Texteingaben abgeleitet werden.
- Felder eines Signal Forms → die
debounce()-Regel. - Throttle (höchstens einmal alle X ms, z. B. Scroll) → RxJS
throttleTimeoderrequestAnimationFrame. - Streams mit mehreren Operatoren → bleib bei RxJS.