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.
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:
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.
- Drücken Sie „Strg + Umschalttaste + J“ (oder „Befehlstaste + Optionstaste + J“ auf einem Mac), um die Entwicklertools zu öffnen.
- 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:
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:
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:
- Profilieren Sie die Anwendung mit den Chrome-Entwicklertools, um herauszufinden, woher die Verzögerungen stammen.
- Einführung der Änderungserkennungsstrategie
OnPush
zum Entfernen von untergeordneten Bäumen einer Komponente. - Verschieben Sie aufwendige Berechnungen in reine Pipes, damit das Framework die berechneten Werte im Cache speichern kann.