Änderungserkennung von Angular optimieren

Schnellere Änderungserkennung für eine bessere Nutzererfahrung implementieren

Angular führt seinen Änderungserkennungsmechanismus regelmäßig aus, damit Änderungen am Datenmodell in der Ansicht einer App berücksichtigt werden. Die Änderungserkennung kann entweder manuell oder über ein asynchrones Ereignis ausgelöst werden, z. B. eine Nutzerinteraktion oder ein XHR-Abschluss.

Die Änderungserkennung ist ein leistungsstarkes Tool. Wenn sie jedoch sehr häufig ausgeführt wird, kann sie viele Berechnungen auslösen und den Hauptbrowser-Thread blockieren.

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

Die Änderungserkennung in Angular

Sehen wir uns anhand einer Beispiel-App an, wie die Änderungserkennung in Angular funktioniert.

Den Code für die App finden Sie in diesem GitHub-Repository.

Die App enthält Mitarbeiter aus zwei Abteilungen eines Unternehmens – Vertrieb und Forschung und Entwicklung – und hat zwei Komponenten:

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

Beispielanwendung

In der Vorlage für AppComponent sind zwei Instanzen von EmployeeListComponent zu sehen:

<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 wir uns nun 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 auch die Methode calculate, die die Geschäftsberechnung implementiert.

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 durchläuft alle Mitarbeiter in der Liste und rendert für jeden ein Listenelement. Außerdem enthält es eine ngModel-Direktive für die bidirektionale Datenbindung zwischen der Eingabe und der in EmployeeListComponent deklarierten label-Property.

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 den Namen eines neuen Mitarbeiters in das Eingabefeld in einer EmployeeListComponent eingibt, löst Angular die Änderungserkennung für den gesamten Komponentenbaum aus, beginnend bei AppComponent. Das bedeutet, dass während der Nutzer die Textzeile eingibt, Angular die mit den einzelnen Mitarbeitern verknüpften numerischen Werte wiederholt neu berechnet, um zu prüfen, ob sie sich seit der letzten Prüfung geändert haben.

Wenn Sie sehen möchten, wie langsam das sein kann, ö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 fibonacci-Funktion zurückzuführen ist, indem Sie das Beispielprojekt einrichten und den Tab Leistung der Chrome-Entwicklertools öffnen.

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

Klicken Sie jetzt links oben im Bereich Leistung auf Aufzeichnen und beginnen Sie, in eines der Textfelder in der App zu tippen. Klicken Sie nach einigen Sekunden noch einmal auf Aufzeichnen , um die Aufzeichnung zu beenden. Sobald Chrome DevTools alle erfassten Profilierungsdaten verarbeitet hat, sehen Sie ungefähr Folgendes:

Leistungsprofilerstellung

Wenn sich viele Mitarbeiter in der Liste befinden, kann dieser Vorgang den UI-Thread des Browsers blockieren und zu Frame-Drops führen, was die Nutzerfreundlichkeit beeinträchtigt.

Unterbäume von Komponenten überspringen

Wenn der Nutzer die Textzeile für die Verkäufe EmployeeListComponent eingibt, wissen Sie, dass sich die Daten in der Forschung und Entwicklung nicht ändern. Es gibt also keinen Grund, die Änderungserkennung für die Komponente auszuführen. Damit die R&D-Instanz keine Änderungserkennung auslöst, legen Sie die changeDetectionStrategy von EmployeeListComponent auf OnPush fest:

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 jetzt eine Textzeile eingibt, wird die Änderungserkennung nur für die entsprechende Abteilung ausgelöst:

Änderungserkennung in einem Komponentenunterbaum

Die Optimierung auf die ursprüngliche Anwendung finden Sie hier.

Weitere Informationen zur OnPush-Strategie zur Änderungserkennung finden Sie in der offiziellen Angular-Dokumentation.

Um die Auswirkungen dieser Optimierung zu sehen, geben Sie einen neuen Mitarbeiter in der Anwendung auf StackBlitz ein.

Reine Pipes verwenden

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

Um dieses Verhalten zu verbessern, können Sie reine Pipes verwenden. Sowohl reine als auch unreine Pipes akzeptieren Eingaben und geben Ergebnisse zurück, die in einer Vorlage verwendet werden können. Der Unterschied besteht darin, dass bei einer reinen Pipe das Ergebnis nur neu berechnet wird, wenn sie eine andere Eingabe als bei der vorherigen Aufruf erhält.

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

So verschieben Sie die Geschäftsberechnung in eine Pipe namens 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 Methode transform der Pipeline ruft die Funktion fibonacci auf. Beachten Sie, dass die Pipeline rein ist. Sofern Sie nichts anderes angeben, betrachtet Angular alle Pipes als rein.

Aktualisieren Sie abschließend den Ausdruck in der Vorlage für EmployeeListComponent:

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

Geschafft! Wenn der Nutzer nun die Texteingabe für eine Abteilung eingibt, wird der numerische Wert für einzelne Mitarbeiter nicht mehr neu berechnet.

In der App unten sehen Sie, wie viel flüssiger das Tippen jetzt ist.

Wenn Sie sich die Auswirkungen der letzten Optimierung ansehen möchten, probieren Sie dieses Beispiel auf StackBlitz aus.

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

Fazit

Wenn Sie in einer Angular-App Laufzeitverlangsamungen feststellen:

  1. Profilieren Sie die Anwendung mit den Chrome-Entwicklertools, um herauszufinden, woher die Verzögerungen stammen.
  2. Einführung der Änderungserkennungsstrategie OnPush zum Entfernen von untergeordneten Bäumen einer Komponente.
  3. Verschieben Sie aufwendige Berechnungen in reine Pipes, damit das Framework die berechneten Werte im Cache speichern kann.