Change Detection ist der Vorgang, mit dem Angular das DOM aktualisiert, wenn sich die Daten ändern. Zwei große Änderungen der letzten Versionen verändern deine Arbeitsweise: In v21 sind neue Anwendungen zoneless (ohne Zone.js), und in v22 sind Komponenten standardmäßig OnPush. Zusammen heißt das: Angular „errät“ nicht mehr, wann sich etwas geändert hat — du musst es ihm sagen, und Signals erledigen das automatisch.
- v21: Neue Projekte enthalten kein
zone.jsmehr. Change Detection wird durch Signals, Events undmarkForCheckangestoßen. - v22: Komponenten ohne
changeDetectionsindOnPush.ChangeDetectionStrategy.Defaultwird zuEager(Default ist deprecated). - ng update auf v22 ergänzt
Eagerautomatisch, wo es nötig ist, damit nichts kaputtgeht.
OnPush als Standard und Eager
// v22: kein `changeDetection` → OnPush
@Component({ selector: 'app-user-card', template: `...` })
export class UserCard {}
// Du willst das alte Verhalten („prüf mich immer“)? Dann explizit:
@Component({
selector: 'app-legacy-widget',
changeDetection: ChangeDetectionStrategy.Eager, // der frühere Default (Default ist deprecated)
template: `...`,
})
export class LegacyWidget {}Was ein Rendering auslöst (zoneless)
Was in einer zoneless Anwendung Change Detection anstößt:
✓ ein im Template gelesenes Signal ändert sich (signal.set/update, input(), resource, toSignal…)
✓ ein Event-Listener im Template oder Host: (click), (input), host: { '(keydown)': … }
✓ die async-Pipe erhält einen neuen Wert
✓ ChangeDetectorRef.markForCheck()
✓ ComponentRef.setInput(), Views werden angehängt / abgehängt
✓ der Router: Navigationen
✗ setTimeout / setInterval / Promise.then, die ein einfaches Feld ändern
✗ subscribe(), das ein einfaches Feld setzt
✗ Callbacks von Drittanbieter-Bibliotheken (WebSocket, Firebase, Maps), die einfache Felder setzen// ✗ Funktioniert mit Zone.js, aber NICHT zoneless + OnPush
export class Clock {
now = new Date();
constructor() {
setInterval(() => (this.now = new Date()), 1000);
}
}
// ✓ Die idiomatische Lösung: ein Signal
export class Clock {
readonly now = signal(new Date());
constructor() {
const id = setInterval(() => this.now.set(new Date()), 1000);
inject(DestroyRef).onDestroy(() => clearInterval(id));
}
}
// ✓ Für Observables: toSignal (oder die async-Pipe)
readonly user = toSignal(this.userService.user$);
// Notlösung für alten Code: markForCheck
this.socket.on('msg', (m) => {
this.messages.push(m);
this.cdr.markForCheck();
});Die OnPush-Regeln
Eine OnPush-Komponente wird nur geprüft, wenn:
1. eines ihrer input() eine NEUE Referenz erhält (Vergleich mit ===)
2. in ihr oder in ihren Kindern ein Event auftritt
3. sich ein in ihrem Template gelesenes Signal ändert
4. die async-Pipe in ihrem Template einen Wert emittiert
5. jemand markForCheck() auf ihr (oder einem Nachfahren) aufruft
Andernfalls wird ihr gesamter Teilbaum übersprungen. Darum ergibt OnPush + Signals minimale Renderings.Mutationen und OnPush
OnPush vergleicht Inputs nach Referenz. Wenn du ein Objekt an Ort und Stelle veränderst, bekommt die Kindkomponente nichts davon mit. Genau dieselbe Regel wie bei Signals: Immutability.
ChangeDetectorRef: wenn du ihn wirklich brauchst
const cdr = inject(ChangeDetectorRef);
cdr.markForCheck(); // „prüf mich in der nächsten Runde“ (asynchron, empfohlen)
cdr.detectChanges(); // „prüf mich JETZT, synchron“ (selten: sofortige Messungen, Tests)
cdr.detach(); // nimmt die Komponente aus der Change Detection (Dashboards mit manuellen Updates)
cdr.reattach();
// v22: cdr.checkNoChanges() wurde aus der öffentlichen API entfernt — in Tests nutze fixture.detectChanges()Ein bestehendes Projekt migrieren
# ng update auf v22 ergänzt automatisch ChangeDetectionStrategy.Eager bei Komponenten,
# die kein changeDetection gesetzt hatten — das Verhalten der Anwendung bleibt identisch.
ng update @angular/core@22 @angular/cli@22
# Danach Komponente für Komponente: Eager entfernen, Zustand in Signals verlagern, testen.
# Für zoneless in einem bestehenden Projekt hat Angular 21 ein Analyse-Tool ergänzt:
# onpush_zoneless_migration (über den Angular-CLI-MCP-Server: ng mcp)Signals sagen Angular, was neu gerendert werden muss: signal() und computed() und ausführlicher Signals unter der Haube. Was eine App langsam macht, Schritt für Schritt: Performance — eine Checkliste.
Wann ja, wann nein
- Jede neue Komponente — es ist ohnehin schon der Standard.
- Der Zustand liegt in Signals, Inputs und Resources.
- Große Listen, Dashboards, Seiten mit viel Inhalt.
Eager: alter, noch nicht migrierter Code, der einfache Felder aus Callbacks ändert.markForCheck: Integration mit Drittanbieter-Bibliotheken, die Signals nicht kennen (besser ist es aber, den Wert in ein Signal zu legen).detectChanges: selten — Tests oder Messungen direkt nach einer Änderung.
Häufige Stolperfallen
- „Es funktioniert erst, wenn ich irgendwo klicke“ = ein einfacher Wert wurde asynchron geändert. Leg ihn in ein Signal.
- Du mutierst ein Objekt, das du über einen Input erhältst → die OnPush-Kindkomponente aktualisiert sich nicht. Erzeuge eine neue Referenz.
- Tests, die sich auf Zone.js verlassen haben (
fakeAsync, impliziteswhenStable): Nutze zonelessawait fixture.whenStable()und die Fake-Timer von Vitest. - Alte Bibliotheken, die Zone.js voraussetzen (z. B. solche, die auf
NgZone.onStablewarten) — prüfe die Kompatibilität, bevor du zone.js entfernst.