Die Änderungserkennung (Change Detection) ist ein fundamentaler Mechanismus in Angular, der bestimmt, wann und wie die Benutzeroberfläche aktualisiert werden muss, um Änderungen im Anwendungszustand widerzuspiegeln. Angular hat im Laufe seiner Entwicklung bedeutende Verbesserungen und Optimierungen in diesem Bereich eingeführt, die die Reaktionsfähigkeit und Leistung von Angular-Anwendungen erheblich steigern.
Angular basiert auf einem reaktiven Programmiermodell, bei dem Änderungen an Datenquellen (wie Benutzereingaben, Netzwerkantworten, Timer-Events usw.) automatisch zu Aktualisierungen in der UI führen. Der Prozess, der diese Synchronisation sicherstellt, ist die Change Detection.
Angular nutzt die Bibliothek Zone.js, um zu erkennen, wann Änderungen auftreten könnten. Zone.js ist ein leistungsstarkes Tool, das:
setTimeout,
setInterval, Promise.then(),
fetch(), und DOM-Events patchtDie Integration mit Zone.js wurde über die Versionen hinweg optimiert, um unnötige Erkennungszyklen zu reduzieren und die Leistung zu verbessern.
Eine wichtige Entwicklung in neueren Angular-Versionen ist die
verbesserte Unterstützung für zonenlose Anwendungen. Mit dem
provideZoneChangeDetection-Provider kann auf Zone.js
verzichtet werden:
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideZoneChangeDetection } from '@angular/core';
bootstrapApplication(AppComponent, {
providers: [
provideZoneChangeDetection({ runCoalescing: false, injectChangeDetection: true }),
]
}).catch(err => console.error(err));Bei diesem Ansatz müssen Änderungen manuell über die injizierte
ChangeDetection-Klasse signalisiert werden:
// In einer Komponente
@Component({
selector: 'app-counter',
template: `<button (click)="increment()">Zähler: {{ count }}</button>`,
})
export class CounterComponent {
count = 0;
constructor(private cd: ChangeDetection) {}
increment() {
this.count++;
this.cd.notify(); // Manuelles Auslösen der Change Detection
}
}Die Angular-Architektur basiert auf einem Komponentenbaum, und die Change Detection folgt dieser hierarchischen Struktur. Wenn eine Änderung erkannt wird:
Diese Top-Down-Propagation gewährleistet einen konsistenten Anwendungszustand während des gesamten Erkennungszyklus.
AppComponent
/ \
Header Content
/ \
NavBar ProductList
/ \
ProductItem ProductItem
Wenn ein Event in der NavBar-Komponente ausgelöst wird, durchläuft die Change Detection den gesamten Baum, es sei denn, bestimmte Optimierungen sind aktiviert.
Angular bietet zwei Hauptstrategien für die Change Detection:
Bei der Standard-Strategie überprüft Angular bei jedem Erkennungszyklus sämtliche Bindungen in der Vorlage einer Komponente:
@Component({
selector: 'app-default-example',
template: `
<h1>{{ title }}</h1>
<p>{{ description }}</p>
<button (click)="updateData()">Aktualisieren</button>
`,
// Standardmäßig ChangeDetectionStrategy.Default
})
export class DefaultExampleComponent {
title = 'Standard Change Detection';
description = 'Diese Komponente verwendet die Default-Strategie';
updateData() {
this.description = 'Beschreibung wurde aktualisiert: ' + new Date().toLocaleTimeString();
}
}Die OnPush-Strategie ist wesentlich effizienter und führt die Change Detection für eine Komponente nur aus, wenn:
@Component({
selector: 'app-onpush-example',
template: `
<h1>{{ title }}</h1>
<p>{{ data.description }}</p>
<button (click)="updateIncorrectly()">Falsch aktualisieren</button>
<button (click)="updateCorrectly()">Richtig aktualisieren</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushExampleComponent {
@Input() data!: { description: string };
title = 'OnPush Change Detection';
constructor(private cdRef: ChangeDetectorRef) {}
// Dies wird NICHT funktionieren, da die Referenz nicht geändert wird
updateIncorrectly() {
this.data.description = 'Aktualisiert: ' + new Date().toLocaleTimeString();
// UI wird NICHT aktualisiert!
}
// Dies funktioniert, da wir ein neues Objekt erstellen
updateCorrectly() {
this.data = {
description: 'Aktualisiert: ' + new Date().toLocaleTimeString()
};
// UI wird aktualisiert!
}
// Alternative: Manuelle Auslösung
forceUpdate() {
this.data.description = 'Manuell aktualisiert: ' + new Date().toLocaleTimeString();
this.cdRef.markForCheck(); // Markiert diese Komponente für die nächste CD
// oder
// this.cdRef.detectChanges(); // Löst sofort CD für diesen Teilbaum aus
}
}Die OnPush-Strategie wurde in neueren Angular-Versionen stetig verbessert:
@Component({
selector: 'app-signals-example',
template: `
<h2>Zähler: {{ counter() }}</h2>
<button (click)="increment()">Erhöhen</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SignalsExampleComponent {
counter = signal(0);
increment() {
this.counter.update(value => value + 1);
// Keine manuelle Change Detection nötig!
}
}Mit Signals wird die Change Detection automatisch ausgelöst, wenn sich ein Signal-Wert ändert, auch in OnPush-Komponenten.
Angular bietet mehrere APIs zur manuellen Steuerung der Change Detection:
Die ChangeDetectorRef-Klasse bietet Methoden zur
direkten Steuerung:
@Component({
selector: 'app-manual-cd',
template: '...',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ManualCDComponent implements OnInit {
data = [];
constructor(
private cdRef: ChangeDetectorRef,
private dataService: DataService
) {}
ngOnInit() {
// Change Detection deaktivieren
this.cdRef.detach();
// Daten laden
this.dataService.getData().subscribe(result => {
this.data = result;
// Manuell Change Detection ausführen
this.cdRef.detectChanges();
// Oder für den nächsten Zyklus markieren
// this.cdRef.markForCheck();
});
}
reattachCD() {
// Change Detection wieder aktivieren
this.cdRef.reattach();
}
}Die NgZone-Klasse ermöglicht es, Code außerhalb der
Angular-Zone auszuführen, um die Change Detection zu umgehen:
@Component({
selector: 'app-zone-example',
template: `
<h2>Aktualisierungszeit: {{ time }}</h2>
<button (click)="startNormalTimer()">Normaler Timer</button>
<button (click)="startOptimizedTimer()">Optimierter Timer</button>
`
})
export class ZoneExampleComponent {
time = new Date().toLocaleTimeString();
constructor(private ngZone: NgZone) {}
// Löst alle 100ms Change Detection aus
startNormalTimer() {
setInterval(() => {
this.time = new Date().toLocaleTimeString();
}, 100);
}
// Löst keine Change Detection aus
startOptimizedTimer() {
this.ngZone.runOutsideAngular(() => {
setInterval(() => {
// UI-Update ohne CD bei jedem Intervall
this.time = new Date().toLocaleTimeString();
// Manuell CD auslösen (nur bei Bedarf)
if (someCondition) {
this.ngZone.run(() => {});
}
}, 100);
});
}
}Angular verstärkt das Prinzip des unidirektionalen Datenflusses, bei dem Daten immer von Eltern- zu Kindkomponenten fließen. Dies sorgt für vorhersehbares Verhalten und vermeidet inkonsistente Zustände:
@Component({
selector: 'app-parent',
template: `
<h2>Parent: {{ parentData }}</h2>
<app-child [data]="parentData" (update)="onChildUpdate($event)"></app-child>
<button (click)="updateParent()">Update Parent</button>
`
})
export class ParentComponent {
parentData = 'Initial parent data';
updateParent() {
this.parentData = 'Updated parent data: ' + new Date().toLocaleTimeString();
}
onChildUpdate(newValue: string) {
this.parentData = newValue;
}
}
@Component({
selector: 'app-child',
template: `
<h3>Child: {{ data }}</h3>
<input [value]="childInput" (input)="onInput($event)">
<button (click)="updateParent()">Send to Parent</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent {
@Input() data!: string;
@Output() update = new EventEmitter<string>();
childInput = '';
onInput(event: any) {
this.childInput = event.target.value;
}
updateParent() {
// Daten werden zum Parent zurückgeschickt, der dann seine eigenen Daten aktualisiert
this.update.emit('From child: ' + this.childInput);
}
}Die Verwendung von unveränderlichen Datenstrukturen ist entscheidend für optimale Performance mit OnPush:
@Component({
selector: 'app-todo-list',
template: `
<h2>Todo List</h2>
<ul>
<li *ngFor="let todo of todos" [class.completed]="todo.completed">
{{ todo.title }}
<button (click)="toggleTodo(todo.id)">Toggle</button>
</li>
</ul>
<button (click)="addTodo()">Add Todo</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoListComponent {
todos: Todo[] = [
{ id: 1, title: 'Learn Angular', completed: false },
{ id: 2, title: 'Master Change Detection', completed: false }
];
// FALSCH: Mutiert das Original-Array
toggleTodoIncorrect(id: number) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
}
// UI wird nicht aktualisiert mit OnPush!
}
// RICHTIG: Erstellt neue Arrays und Objekte
toggleTodo(id: number) {
this.todos = this.todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
);
// UI wird aktualisiert!
}
addTodo() {
const newTodo: Todo = {
id: Date.now(),
title: 'New Todo ' + this.todos.length,
completed: false
};
this.todos = [...this.todos, newTodo];
}
}
interface Todo {
id: number;
title: string;
completed: boolean;
}Die AsyncPipe ist eine ideale Ergänzung zur OnPush-Strategie, da sie automatisch die Change Detection auslöst, wenn ein Observable neue Daten emittiert:
@Component({
selector: 'app-user-dashboard',
template: `
<div *ngIf="users$ | async as users; else loading">
<h2>Benutzer ({{ users.length }})</h2>
<ul>
<li *ngFor="let user of users">{{ user.name }}</li>
</ul>
<button (click)="refresh()">Aktualisieren</button>
</div>
<ng-template #loading>Lade Daten...</ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserDashboardComponent implements OnInit {
users$!: Observable<User[]>;
constructor(private userService: UserService) {}
ngOnInit() {
this.users$ = this.userService.getUsers().pipe(
// RxJS-Operatoren für zusätzliche Logik
map(users => users.sort((a, b) => a.name.localeCompare(b.name))),
shareReplay(1) // Caching des letzten Ergebnisses
);
}
refresh() {
// Einfaches Neuladen der Daten
this.users$ = this.userService.getUsers().pipe(
map(users => users.sort((a, b) => a.name.localeCompare(b.name))),
shareReplay(1)
);
}
}
interface User {
id: number;
name: string;
}Neuere Angular-Versionen haben die Unterstützung für Signals eingeführt, eine Alternative zum traditionellen Change Detection-Mechanismus:
@Component({
selector: 'app-signal-counter',
template: `
<h2>Zähler: {{ counter() }}</h2>
<p>Quadriert: {{ squared() }}</p>
<button (click)="increment()">+1</button>
<button (click)="decrement()">-1</button>
`,
// Signals funktionieren mit beiden CD-Strategien
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SignalCounterComponent {
// Signal mit initialem Wert
counter = signal(0);
// Computed-Signal (wird automatisch neu berechnet)
squared = computed(() => this.counter() * this.counter());
// Effect (wird bei jeder Änderung des Signals ausgeführt)
constructor() {
effect(() => {
console.log(`Zähler geändert: ${this.counter()}`);
});
}
increment() {
// Aktualisierung des Signals
this.counter.update(value => value + 1);
}
decrement() {
this.counter.update(value => value - 1);
}
// Signal mit Objekt
userProfile = signal<UserProfile>({
name: 'Max',
age: 30
});
updateProfile() {
// Immutable Update eines Objekts
this.userProfile.update(profile => ({
...profile,
age: profile.age + 1
}));
}
}
interface UserProfile {
name: string;
age: number;
}Signals bieten mehrere Vorteile:
Angular bietet Tools zum Debuggen der Change Detection:
In den Angular DevTools (Chrome-Erweiterung) können Sie:
// In einer Komponente
@Component({
selector: 'app-debug-example',
template: '...',
})
export class DebugExampleComponent implements AfterViewChecked {
private previousValue: any;
constructor() {
// Change Detection-Ereignisse loggen
(window as any).ng.profiler.timeChangeDetection();
}
ngAfterViewChecked() {
// Überwachen, wie oft die Komponente aktualisiert wird
console.log('Change Detection ausgeführt');
// Logik zur Erkennung unnötiger Updates
if (this.currentValue === this.previousValue) {
console.warn('Unnötige Change Detection erkannt!');
}
this.previousValue = this.currentValue;
}
}Verwenden Sie ChangeDetectionStrategy.OnPush für alle
Komponenten standardmäßig:
// In neueren Angular-Versionen können Sie dies global konfigurieren
// main.ts
import { bootstrapApplication, ChangeDetectionStrategy } from '@angular/platform-browser';
import { DEFAULT_CHANGE_DETECTION_STRATEGY, provideGlobalChangeDetectionConfig } from '@angular/core';
bootstrapApplication(AppComponent, {
providers: [
provideGlobalChangeDetectionConfig({
defaultStrategy: ChangeDetectionStrategy.OnPush
})
]
});@Component({
selector: 'app-performance',
template: `<div>{{ displayValue }}</div>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PerformanceComponent implements OnInit {
displayValue = '';
private data: ComplexData[] = [];
constructor(private ngZone: NgZone, private dataService: DataService) {}
ngOnInit() {
// Daten laden und verarbeiten außerhalb der Angular-Zone
this.ngZone.runOutsideAngular(() => {
this.dataService.getComplexData().subscribe(data => {
this.data = data;
// Schwere Berechnung
const processedValue = this.processData(data);
// Zurück in die Angular-Zone für UI-Update
this.ngZone.run(() => {
this.displayValue = processedValue;
});
});
});
}
private processData(data: ComplexData[]): string {
// Hier komplexe, zeitaufwändige Berechnung
// ...
return result;
}
}@Component({
selector: 'app-virtual-scroll',
template: `
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
<div *cdkVirtualFor="let item of items" class="item">
{{ item.name }}
</div>
</cdk-virtual-scroll-viewport>
`,
styles: [`
.viewport { height: 500px; width: 100%; }
.item { height: 50px; }
`],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class VirtualScrollComponent {
items = Array.from({length: 10000}).map((_, i) => ({
id: i, name: `Item #${i}`
}));
}@Component({
selector: 'app-track-by-example',
template: `
<ul>
<li *ngFor="let user of users; trackBy: trackByUserId">
{{ user.name }}
</li>
</ul>
<button (click)="refresh()">Aktualisieren</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TrackByExampleComponent {
users: User[] = [];
constructor(private userService: UserService) {
this.loadUsers();
}
trackByUserId(index: number, user: User): number {
return user.id;
}
refresh() {
this.loadUsers();
}
private loadUsers() {
this.userService.getUsers().subscribe(users => {
this.users = users;
});
}
}@Component({
selector: 'app-optimized-dashboard',
template: `
<div class="dashboard">
<div class="stats">
<h2>Statistiken</h2>
<div>Aktive Benutzer: {{ activeUsers() }}</div>
<div>Neue Registrierungen: {{ newRegistrations() }}</div>
</div>
<div class="user-list">
<h2>Benutzer</h2>
<div *ngIf="users$ | async as users">
<ul>
<li *ngFor="let user of users; trackBy: trackById"
(click)="selectUser(user.id)">
{{ user.name }} {{ user.id === selectedUserId() ? '(ausgewählt)' : '' }}
</li>
</ul>
</div>
</div>
<div class="detail-view" *ngIf="selectedUser$ | async as user">
<h2>Details für {{ user.name }}</h2>
<pre>{{ user | json }}</pre>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OptimizedDashboardComponent implements OnInit {
// Signals
activeUsers = signal(0);
newRegistrations = signal(0);
selectedUserId = signal<number | null>(null);
// Observables
users$ = this.userService.getUsers();
selectedUser$ = computed(() => {
return this.selectedUserId() !== null
? this.userService.getUserById(this.selectedUserId()!)
: of(null);
});
constructor(private userService: UserService) {}
ngOnInit() {
// Stats aktualisieren
this.userService.getStats().subscribe(stats => {
this.activeUsers.set(stats.activeUsers);
this.newRegistrations.set(stats.newRegistrations);
});
}
selectUser(id: number) {
this.selectedUserId.set(id);
}
trackById(index: number, item: any): number {
return item.id;
}
}Die Change Detection bietet:
Mit einem guten Verständnis der Change Detection und der Anwendung der Best Practices können Sie hochperformante und reaktionsschnelle Angular-Anwendungen entwickeln, die selbst bei komplexen UIs und großen Datenmengen effizient bleiben.