12 Change Detection

12.1 Einführung und Überblick

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.

12.2 Zone.js und der Erkennungsmechanismus

12.2.1 Funktionsweise von Zone.js

Angular nutzt die Bibliothek Zone.js, um zu erkennen, wann Änderungen auftreten könnten. Zone.js ist ein leistungsstarkes Tool, das:

Die Integration mit Zone.js wurde über die Versionen hinweg optimiert, um unnötige Erkennungszyklen zu reduzieren und die Leistung zu verbessern.

12.2.2 Zoneless-Ansatz in Angular

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
  }
}

12.3 Hierarchische Change Detection

12.3.1 Komponentenbaum und Change Detection

Die Angular-Architektur basiert auf einem Komponentenbaum, und die Change Detection folgt dieser hierarchischen Struktur. Wenn eine Änderung erkannt wird:

  1. Startet Angular die Change Detection bei der Wurzelkomponente
  2. Arbeitet sich durch den Komponentenbaum nach unten vor
  3. Überprüft jede Komponente auf Änderungen basierend auf ihrer Erkennungsstrategie

Diese Top-Down-Propagation gewährleistet einen konsistenten Anwendungszustand während des gesamten Erkennungszyklus.

12.3.2 Visualisierung des Erkennungsprozesses

        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.

12.4 Change Detection Strategien in Angular

Angular bietet zwei Hauptstrategien für die Change Detection:

12.4.1 Default-Strategie

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();
  }
}

12.4.2 OnPush-Strategie

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
  }
}

12.5 Verbesserungen der OnPush-Strategie

Die OnPush-Strategie wurde in neueren Angular-Versionen stetig verbessert:

  1. Optimierte Erkennung: Intelligenteren Algorithmen reduzieren die Anzahl der Komponenten, die überprüft werden müssen
  2. Verbesserte Debugging-Tools: DevTools-Integration zur Visualisierung der Change Detection
  3. Signal-basierte Reaktivität: Bessere Integration mit dem Signals-API

12.5.1 Beispiel für Signal-Integration mit OnPush

@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.

12.6 Manuelle Steuerung der Change Detection

Angular bietet mehrere APIs zur manuellen Steuerung der Change Detection:

12.6.1 ChangeDetectorRef

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();
  }
}

12.6.2 NgZone

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);
    });
  }
}

12.7 Unidirektionaler Datenfluss

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);
  }
}

12.8 Immutabilität und Performance-Optimierung

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;
}

12.9 RxJS und AsyncPipe

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;
}

12.10 Signals in Angular

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:

  1. Feingranulare Reaktivität: Nur abhängige Teile werden aktualisiert
  2. Automatische Abhängigkeitsverfolgung: Keine manuelle Verwaltung von Subscriptions
  3. Kompatibilität mit Zone.js und zoneless: Funktioniert in beiden Modi
  4. Verbesserte Performance: Reduziert unnötige Render-Zyklen

12.11 Debugging der Change Detection

Angular bietet Tools zum Debuggen der Change Detection:

12.11.1 DevTools-Integration

In den Angular DevTools (Chrome-Erweiterung) können Sie:

  1. Change Detection-Zyklen in Echtzeit beobachten
  2. Die Ausführungszeit pro Komponente analysieren
  3. Manuelle CD-Zyklen auslösen
  4. Komponenten-Profiling durchführen

12.11.2 Programmatisches Debugging

// 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;
  }
}

12.12 Best Practices für optimale Performance

12.12.1 OnPush als Standard verwenden

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
    })
  ]
});

12.12.2 Schwere Berechnungen aus der Change Detection heraushalten

@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;
  }
}

12.12.3 Virtuelle Scrolling für große Listen

@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}`
  }));
}

12.12.4 trackBy für NgFor verwenden

@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;
    });
  }
}

12.12.5 Kombination von Signals, OnPush und AsyncPipe

@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:

  1. Verbesserte Performance: Durch optimierte Algorithmen und Strategien
  2. Flexible Strategien: Default für Einfachheit, OnPush für bessere Leistung
  3. Manuelle Kontrolle: Präzise Steuerung durch ChangeDetectorRef und NgZone
  4. Signals-Integration: Feingranulare Reaktivität ohne komplexes Boilerplate
  5. Bessere Tooling: DevTools für Diagnose und Optimierung

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.