Implementieren Sie eine schnellere Änderungserkennung, um die Nutzererfahrung zu verbessern.
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 oft ausgeführt wird, kann sie viele Berechnungen auslösen und den Hauptthread des Browsers 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.
Änderungserkennung von Angular
Sehen wir uns eine Beispiel-App an, um zu verstehen, wie die Änderungserkennung von Angular funktioniert.
Den Code für die Anwendung 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 F&E.
Sie können die beiden Instanzen von EmployeeListComponent
in der Vorlage für AppComponent
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 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 den Namen einer Abteilung als Eingaben. Wenn der Nutzer versucht, einen Mitarbeiter zu entfernen oder hinzuzufügen, löst die Komponente eine entsprechende Ausgabe aus. Die Komponente definiert auch 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 durchläuft alle Mitarbeitenden in der Liste und gibt für jeden ein Listenelement wieder. Außerdem enthält sie 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 Anwendung die folgende 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.
Um zu sehen, wie langsam das sein kann, öffnen Sie die nicht optimierte Version des Projekts auf StackBlitz und geben Sie den Namen eines Mitarbeiters ein.
Sie können prüfen, ob die Verlangsamung auf die Funktion fibonacci
zurückzuführen ist. Richten Sie dazu das Beispielprojekt ein und öffnen Sie in den Chrome-Entwicklertools den Tab Leistung.
- Drücken Sie „Strg + Umschalttaste + J“ (oder „Befehlstaste + Option + 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 die Chrome-Entwicklertools alle erhobenen Profildaten verarbeitet haben, wird Folgendes angezeigt:
Wenn die Liste viele Mitarbeiter enthält, kann dieser Vorgang den UI-Thread des Browsers blockieren und Frame-Ausfälle verursachen, was zu einer schlechten Nutzererfahrung führt.
Komponenten-Unterstrukturen ü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 ein Nutzer nun eine Texteingabe eingibt, wird die Änderungserkennung nur für die entsprechende Abteilung ausgelöst:
Hier finden Sie die auf die ursprüngliche Anwendung angewendete Optimierung.
Weitere Informationen zur Änderungserkennungsstrategie von OnPush
finden Sie in der offiziellen Angular-Dokumentation.
Um die Auswirkungen dieser Optimierung zu sehen, geben Sie einen neuen Mitarbeiter in die Bewerbung auf StackBlitz ein.
Reine Pipes verwenden
Auch wenn die Strategie zur Änderungserkennung für EmployeeListComponent
jetzt auf OnPush
gesetzt ist, berechnet Angular den numerischen Wert für alle Mitarbeiter in einer Abteilung neu, wenn der Nutzer die entsprechende Texteingabe eingibt.
Zur Verbesserung dieses Verhaltens 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 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.
Zur Erinnerung: Die App berechnet anhand des numerischen Werts des Mitarbeiters einen anzuzeigenden Wert und ruft die in EmployeeListComponent
definierte Methode calculate
auf. Wenn Sie die Berechnung in eine reine Pipe verschieben, berechnet Angular den Pipe-Ausdruck nur dann neu, wenn sich seine Argumente ändern. Das Framework bestimmt durch Ausführung einer Referenzprüfung, ob sich die Argumente der Pipe geändert haben. Das bedeutet, dass Angular keine Neuberechnungen durchführt, solange der numerische Wert für einen Mitarbeiter nicht aktualisiert wird.
So verschieben 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 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 zuletzt 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, wird der numerische Wert für einzelne Mitarbeiter nicht mehr neu berechnet.
In der App unten kannst du sehen, wie viel flüssiger du tippst.
Wenn Sie die Auswirkungen der letzten Optimierung sehen 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 mit Laufzeitverlangsamungen in einer Angular-App konfrontiert werden:
- Erstellen Sie ein Profil der Anwendung mit den Chrome-Entwicklertools, um zu sehen, woher die Abschwünge kommen.
- Mit der Änderungserkennungsstrategie
OnPush
die Unterstrukturen einer Komponente entfernen - Verschieben Sie aufwendige Berechnungen in reine Pipes, damit das Framework die berechneten Werte im Cache speichern kann.