Implementieren Sie eine schnellere Änderungserkennung, um die Nutzererfahrung zu verbessern.
Angular führt seinen Änderungserkennungsmechanismus regelmäßig aus, sodass Änderungen am Datenmodell in der Ansicht einer App berücksichtigt werden. Die Änderungserkennung kann entweder manuell oder durch ein asynchrones Ereignis ausgelöst werden, z. B. eine Nutzerinteraktion oder einen 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 Änderungserkennungsmechanismus steuern und optimieren können, 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 zu verstehen, wie die Änderungserkennung von Angular funktioniert.
Den Code für die Anwendung finden Sie in diesem GitHub-Repository.
Die App listet Mitarbeitende aus zwei Abteilungen eines Unternehmens – Vertrieb und F&E – auf und enthält zwei Komponenten:
AppComponent
, der 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 beginnt, den Namen eines neuen Mitarbeiters in das Eingabefeld in einem EmployeeListComponent
einzugeben, löst Angular die Änderungserkennung für die gesamte Komponentenstruktur ab AppComponent
aus. Das bedeutet, dass Angular die numerischen Werte jedes Mitarbeiters während der Texteingabe wiederholt neu berechnet, um zu prüfen, ob sich diese seit der letzten Prüfung nicht 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 nun oben links im Steuerfeld Leistung auf Aufzeichnen und beginnen Sie mit der Eingabe in einem der Textfelder in der App. Klicken Sie nach einigen Sekunden noch einmal auf Aufzeichnen , um die Aufzeichnung zu beenden. Sobald die Chrome-Entwicklertools alle erhobenen Profildaten verarbeitet haben, sehen Sie Folgendes:
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 den Text für Umsatz EmployeeListComponent
eingibt, wissen Sie, dass sich die Daten in der Abteilung F&E nicht ändern. Daher gibt es keinen Grund, die Änderungserkennung für die entsprechende Komponente auszuführen. Damit die F&E-Instanz keine Änderungserkennung auslöst, legen Sie 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 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.
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 Pipe ruft die Funktion fibonacci
auf. Beachten Sie, dass die Pipe rein ist. Angular betrachtet alle Pipes als rein, sofern Sie nichts anderes angeben.
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, berechnet die App den numerischen Wert für einzelne Mitarbeiter nicht neu.
In der App unten kannst du sehen, wie viel flüssiger du tippst.
Um die Auswirkungen der letzten Optimierung zu sehen, probieren Sie dieses Beispiel bei 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 ein Caching der berechneten Werte durchführen kann.