Änderungserkennung von Angular optimieren

Implementieren Sie eine schnellere Änderungserkennung für eine bessere Nutzererfahrung.

Angular führt seinen Mechanismus zur Änderungserkennung regelmäßig aus, damit Änderungen am Datenmodell in der Ansicht einer Anwendung widergespiegelt werden. Die Änderungserkennung kann entweder manuell oder durch ein asynchrones Ereignis (z. B. eine Nutzerinteraktion oder einen XHR-Abschluss) ausgelöst werden.

Die Änderungserkennung ist ein leistungsstarkes Tool. Wird es jedoch sehr häufig ausgeführt, kann es viele Berechnungen auslösen und den Browser-Hauptthread blockieren.

In diesem Beitrag erfahren Sie, wie Sie den Mechanismus zur Änderungserkennung steuern und optimieren, indem Sie Teile Ihrer Anwendung überspringen und die Änderungserkennung nur bei Bedarf ausführen.

Informationen zur Änderungserkennung von Angular

Sehen wir uns eine Beispiel-App an, um die Funktionsweise der Änderungserkennung von Angular zu verstehen.

Sie finden den Code für die Anwendung in diesem GitHub-Repository.

Die App listet Mitarbeitende aus zwei Abteilungen eines Unternehmens (Vertrieb und F&E) auf und besteht aus zwei Komponenten:

  • AppComponent, die Stammkomponente der App, und
  • Zwei Instanzen von EmployeeListComponent, eine für Vertrieb und eine für Forschung und Entwicklung.

Beispielanwendung

Sie sehen die beiden Instanzen von EmployeeListComponent in der Vorlage für AppComponent:

<app-employee-list
  [data]="salesList"
  department="Sales"
  (add)="add(salesList, $event)"
  (remove)="remove(salesList, $event)"
></app-employee-list>

<app-employee-list
  [data]="rndList"
  department="R&D"
  (add)="add(rndList, $event)"
  (remove)="remove(rndList, $event)"
></app-employee-list>

Für jeden Mitarbeiter gibt es einen Namen und einen numerischen Wert. Die App übergibt den numerischen Wert des Mitarbeiters an eine Geschäftsberechnung und visualisiert das Ergebnis auf dem Bildschirm.

Sehen Sie sich jetzt EmployeeListComponent an:

const fibonacci = (num: number): number => {
  if (num === 1 || num === 2) {
    return 1;
  }
  return fibonacci(num - 1) + fibonacci(num - 2);
};

@Component(...)
export class EmployeeListComponent {
  @Input() data: EmployeeData[];
  @Input() department: string;
  @Output() remove = new EventEmitter<EmployeeData>();
  @Output() add = new EventEmitter<string>();

  label: string;

  handleKey(event: any) {
    if (event.keyCode === 13) {
      this.add.emit(this.label);
      this.label = '';
    }
  }

  calculate(num: number) {
    return fibonacci(num);
  }
}

EmployeeListComponent akzeptiert eine Liste von Mitarbeitern und einen Abteilungsnamen als Eingabe. Wenn der Nutzer versucht, einen Mitarbeiter zu entfernen oder hinzuzufügen, löst die Komponente eine entsprechende Ausgabe aus. Die Komponente definiert außerdem die calculate-Methode, mit der die Geschäftsberechnung implementiert wird.

Hier ist die Vorlage für EmployeeListComponent:

<h1 title="Department">{{ department }}</h1>
<mat-form-field>
  <input placeholder="Enter name here" matInput type="text" [(ngModel)]="label" (keydown)="handleKey($event)">
</mat-form-field>
<mat-list>
  <mat-list-item *ngFor="let item of data">
    <h3 matLine title="Name">
      {{ item.label }}
    </h3>
    <md-chip title="Score" class="mat-chip mat-primary mat-chip-selected" color="primary" selected="true">
      {{ calculate(item.num) }}
    </md-chip>
  </mat-list-item>
</mat-list>

Dieser Code iteriert über alle Mitarbeiter in der Liste und rendert für jeden Mitarbeiter ein Listenelement. Sie enthält auch eine ngModel-Anweisung für die bidirektionale Datenbindung zwischen der Eingabe und der in EmployeeListComponent deklarierten Eigenschaft label.

Mit den beiden Instanzen von EmployeeListComponent bildet die App den folgenden Komponentenbaum:

Komponentenstruktur

AppComponent ist die Stammkomponente der Anwendung. Die untergeordneten Komponenten sind die beiden Instanzen von EmployeeListComponent. Jede Instanz hat eine Liste von Elementen (E1, E2 usw.), die die einzelnen Mitarbeiter in der Abteilung repräsentieren.

Wenn der Nutzer beginnt, den Namen eines neuen Mitarbeiters in das Eingabefeld eines EmployeeListComponent einzugeben, löst Angular die Änderungserkennung für den gesamten Komponentenbaum ab AppComponent aus. Das bedeutet, dass Angular während der Texteingabe wiederholt die numerischen Werte jedes Mitarbeiters neu berechnet, um sicherzustellen, dass sich die Werte seit der letzten Überprüfung nicht geändert haben.

Wenn Sie wissen möchten, wie lange das dauert, öffnen Sie die nicht optimierte Version des Projekts auf StackBlitz und geben Sie einen Mitarbeiternamen ein.

Sie können prüfen, ob die Verlangsamung auf die Funktion fibonacci zurückzuführen ist. Dazu richten Sie das Beispielprojekt ein und öffnen den Tab Leistung der Chrome-Entwicklertools.

  1. Drücken Sie Strg + Umschalttaste + J (oder Befehlstaste + Option + J auf dem Mac), um die Entwicklertools zu öffnen.
  2. Klicken Sie auf den Tab Leistung.

, um die Aufzeichnung zu beenden. Sobald die Chrome-Entwicklertools alle erfassten Profildaten verarbeitet haben, sehen Sie in etwa Folgendes:

Leistungsprofilerstellung

Wenn die Liste viele Mitarbeiter enthält, kann dieser Vorgang den UI-Thread des Browsers blockieren und dazu führen, dass Frames ausfallen, was zu einer schlechten Nutzererfahrung führt.

Komponentenunterstrukturen überspringen

Wenn der Nutzer den Text für Vertrieb EmployeeListComponent eingibt, wissen Sie, dass sich die Daten in der Abteilung Forschung und Entwicklung nicht ändern. Es gibt also keinen Grund, eine Änderungserkennung für die Komponente auszuführen. Damit die F&E-Instanz keine Änderungserkennung auslöst, setzen Sie changeDetectionStrategy von EmployeeListComponent auf OnPush:

import { ChangeDetectionStrategy, ... } from '@angular/core';

@Component({
  selector: 'app-employee-list',
  template: `...`,
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrls: ['employee-list.component.css']
})
export class EmployeeListComponent {...}

Wenn der Nutzer nun eine Texteingabe eingibt, wird die Änderungserkennung nur für die entsprechende Abteilung ausgelöst:

Änderungserkennung in einer Komponentenunterstruktur

Diese auf die ursprüngliche Anwendung angewendete Optimierung finden Sie hier.

Weitere Informationen zur Änderungserkennungsstrategie für OnPush finden Sie in der offiziellen Angular-Dokumentation.

Um die Auswirkungen dieser Optimierung zu sehen, gib einen neuen Mitarbeiter in die Bewerbung auf StackBlitz ein.

Reine Pipes verwenden

Obwohl die Strategie zur Änderungserkennung für EmployeeListComponent jetzt auf OnPush gesetzt ist, berechnet Angular den numerischen Wert für alle Mitarbeiter in einer Abteilung trotzdem neu, wenn der Nutzer die entsprechende Texteingabe eingibt.

Um dieses Verhalten zu verbessern, können Sie reine Pipes nutzen. Sowohl reine als auch unreine Pipes akzeptieren Eingaben und geben Ergebnisse zurück, die in einer Vorlage verwendet werden können. Der Unterschied zwischen den beiden besteht darin, dass eine reine Pipe ihr Ergebnis nur dann neu berechnet, wenn sie eine andere Eingabe vom vorherigen Aufruf erhält.

Denken Sie daran, dass die App einen anzuzeigenden Wert basierend auf dem numerischen Wert des Mitarbeiters berechnet und die in EmployeeListComponent definierte Methode calculate aufruft. Wenn Sie die Berechnung in eine reine Pipe verschieben, berechnet Angular den Pipe-Ausdruck nur dann neu, wenn sich seine Argumente ändern. Das Framework ermittelt, ob sich die Argumente der Pipe geändert haben, indem es eine Referenzprüfung durchführt. Das bedeutet, dass Angular erst dann Neuberechnungen durchführt, wenn der numerische Wert für einen Mitarbeiter aktualisiert wird.

So übertragen Sie die Geschäftsberechnung in eine Pipe mit dem Namen CalculatePipe:

import { Pipe, PipeTransform } from '@angular/core';

const fibonacci = (num: number): number => {
  if (num === 1 || num === 2) {
    return 1;
  }
  return fibonacci(num - 1) + fibonacci(num - 2);
};

@Pipe({
  name: 'calculate'
})
export class CalculatePipe implements PipeTransform {
  transform(val: number) {
    return fibonacci(val);
  }
}

Die transform-Methode der Pipe ruft die fibonacci-Funktion auf. Beachten Sie, dass die Pipe rein ist. Bei Angular werden alle Pipes als rein betrachtet, sofern Sie nichts anderes angeben.

Aktualisieren Sie schließlich den Ausdruck in der Vorlage für EmployeeListComponent:

<mat-chip-list>
  <md-chip>
    {{ item.num | calculate }}
  </md-chip>
</mat-chip-list>

Fertig! Wenn der Nutzer nun die Texteingabe für eine Abteilung eingibt, berechnet die App den numerischen Wert für einzelne Mitarbeiter nicht neu.

In der App unten kannst du sehen, wie viel flüssiger das Tippen ist!

Um die Auswirkungen der letzten Optimierung zu sehen, probieren Sie dieses Beispiel auf StackBlitz aus.

Den Code mit der reinen Pipe-Optimierung der ursprünglichen Anwendung ist hier verfügbar.

Fazit

Bei verlangsamten Laufzeiten in einer Angular-App:

  1. Erstellen Sie mit den Chrome-Entwicklertools ein Profil der Anwendung, um zu sehen, woher die Verzögerungen kommen.
  2. Führen Sie die Änderungserkennungsstrategie OnPush ein, um die Unterstruktur einer Komponente zu bereinigen.
  3. Verschieben Sie schwere Berechnungen in reine Pipes, damit das Framework die berechneten Werte im Cache speichern kann.