Implémentez une détection des modifications plus rapide pour améliorer l'expérience utilisateur.
Angular exécute son mécanisme de détection des modifications périodiquement afin que les modifications apportées au modèle de données soient reflétées dans la vue d'une application. La détection des modifications peut être déclenchée manuellement ou par le biais d'un événement asynchrone (par exemple, une interaction utilisateur ou une fin de requête XHR).
La détection des modifications est un outil puissant, mais si elle est exécutée très souvent, elle peut déclencher de nombreux calculs et bloquer le thread principal du navigateur.
Dans cet article, vous allez découvrir comment contrôler et optimiser le mécanisme de détection des modifications en ignorant certaines parties de votre application et en exécutant la détection des modifications uniquement lorsque cela est nécessaire.
Fonctionnement de la détection des changements dans Angular
Pour comprendre le fonctionnement de la détection des modifications d'Angular, examinons un exemple d'application.
Vous trouverez le code de l'application dans ce dépôt GitHub.
L'application liste les employés de deux services d'une entreprise (ventes et R&D) et comporte deux composants :
AppComponent
, qui est le composant racine de l'application, et- Deux instances de
EmployeeListComponent
, une pour les ventes et une pour la R&D.
Vous pouvez voir les deux instances de EmployeeListComponent
dans le modèle pour 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>
Chaque employé est associé à un nom et à une valeur numérique. L'application transmet la valeur numérique de l'employé à un calcul commercial et visualise le résultat à l'écran.
Examinons maintenant EmployeeListComponent
:
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
accepte une liste d'employés et un nom de service comme entrées. Lorsque l'utilisateur tente de supprimer ou d'ajouter un employé, le composant déclenche une sortie correspondante. Le composant définit également la méthode calculate
, qui implémente le calcul métier.
Voici le modèle pour 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>
Ce code effectue une itération sur tous les employés de la liste et, pour chacun d'eux, affiche un élément de liste. Il inclut également une directive ngModel
pour la liaison de données bidirectionnelle entre l'entrée et la propriété label
déclarée dans EmployeeListComponent
.
Avec les deux instances de EmployeeListComponent
, l'application forme l'arborescence de composants suivante :
AppComponent
est le composant racine de l'application. Ses composants enfants sont les deux instances de EmployeeListComponent
. Chaque instance comporte une liste d'éléments (E1, E2, etc.) qui représentent les employés individuels du service.
Lorsque l'utilisateur commence à saisir le nom d'un nouvel employé dans la zone de saisie d'unEmployeeListComponent
, Angular déclenche la détection des modifications pour l'ensemble de l'arborescence des composants à partir de AppComponent
. Cela signifie que pendant que l'utilisateur saisit du texte, Angular recalcule à plusieurs reprises les valeurs numériques associées à chaque employé pour vérifier qu'elles n'ont pas changé depuis la dernière vérification.
Pour vous rendre compte de la lenteur de cette opération, ouvrez la version non optimisée du projet sur StackBlitz et essayez de saisir le nom d'un employé.
Vous pouvez vérifier que le ralentissement provient de la fonction fibonacci
en configurant l'exemple de projet et en ouvrant l'onglet Performances des outils pour les développeurs Chrome.
- Appuyez sur Ctrl+Maj+J (ou Cmd+Option+J sur Mac) pour ouvrir les outils de développement.
- Cliquez sur l'onglet Performances.
Cliquez ensuite sur Enregistrer (en haut à gauche du panneau Performances), puis commencez à saisir du texte dans l'une des zones de texte de l'application. Au bout de quelques secondes, cliquez de nouveau sur Enregistrer pour arrêter l'enregistrement. Une fois que les outils de développement Chrome ont traité toutes les données de profilage qu'ils ont collectées, vous devriez voir quelque chose comme ceci :
Si la liste contient de nombreux employés, ce processus peut bloquer le thread d'UI du navigateur et entraîner des pertes d'images, ce qui nuit à l'expérience utilisateur.
Ignorer les sous-arbres de composants
Lorsque l'utilisateur saisit du texte dans le champ de saisie pour les ventes EmployeeListComponent
, vous savez que les données du service R&D ne changent pas. Il n'y a donc aucune raison d'exécuter la détection des modifications sur son composant. Pour vous assurer que l'instance R&D ne déclenche pas la détection des modifications, définissez le changeDetectionStrategy
de EmployeeListComponent
sur OnPush
:
import { ChangeDetectionStrategy, ... } from '@angular/core';
@Component({
selector: 'app-employee-list',
template: `...`,
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['employee-list.component.css']
})
export class EmployeeListComponent {...}
Désormais, lorsque l'utilisateur saisit du texte, la détection des modifications n'est déclenchée que pour le service correspondant :
Pour voir cette optimisation appliquée à l'application d'origine, cliquez ici.
Pour en savoir plus sur la stratégie de détection des modifications OnPush
, consultez la documentation officielle d'Angular.
Pour voir l'effet de cette optimisation, saisissez un nouvel employé dans l'application sur StackBlitz.
Utiliser des pipes purs
Même si la stratégie de détection des modifications pour EmployeeListComponent
est désormais définie sur OnPush
, Angular recalcule toujours la valeur numérique pour tous les employés d'un service lorsque l'utilisateur saisit le texte correspondant.
Pour améliorer ce comportement, vous pouvez tirer parti des pipes purs. Les pipes purs et impurs acceptent les entrées et renvoient des résultats qui peuvent être utilisés dans un modèle. La différence entre les deux est qu'un pipe pur ne recalculera son résultat que s'il reçoit une entrée différente de son invocation précédente.
N'oubliez pas que l'application calcule une valeur à afficher en fonction de la valeur numérique de l'employé, en appelant la méthode calculate
définie dans EmployeeListComponent
. Si vous déplacez le calcul vers un pipe pur, Angular recalculera l'expression du pipe uniquement lorsque ses arguments changeront. Le framework détermine si les arguments du pipe ont changé en effectuant une vérification de référence. Cela signifie qu'Angular n'effectuera aucun nouveau calcul, sauf si la valeur numérique d'un employé est modifiée.
Voici comment déplacer le calcul de l'entreprise vers un pipeline appelé 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);
}
}
La méthode transform
du pipe appelle la fonction fibonacci
. Notez que le pipe est pur. Angular considère que tous les pipes sont purs, sauf indication contraire.
Enfin, mettez à jour l'expression dans le modèle pour EmployeeListComponent
:
<mat-chip-list>
<md-chip>
{{ item.num | calculate }}
</md-chip>
</mat-chip-list>
Et voilà ! Désormais, lorsque l'utilisateur saisit du texte dans le champ de saisie associé à un service, l'application ne recalcule pas la valeur numérique pour chaque employé.
Dans l'application ci-dessous, vous pouvez constater que la saisie est beaucoup plus fluide.
Pour voir l'effet de la dernière optimisation, essayez cet exemple sur StackBlitz.
Le code avec l'optimisation du canal pur de l'application d'origine est disponible ici.
Conclusion
En cas de ralentissement de l'exécution dans une application Angular :
- Profilez l'application avec les outils pour les développeurs Chrome afin d'identifier les sources de ralentissement.
- Introduisez la stratégie de détection des modifications
OnPush
pour élaguer les sous-arbres d'un composant. - Déplacez les calculs complexes vers des pipes purs pour permettre au framework de mettre en cache les valeurs calculées.