Optimiser la détection des modifications dans Angular

Détecter les modifications plus rapidement pour améliorer l'expérience utilisateur.

Angular exécute régulièrement son mécanisme de détection des modifications afin que les modifications apportées au modèle de données soient répercuté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 de l'utilisateur ou la confirmation d'une requête XHR).

La détection des modifications est un outil puissant, mais s'il est exécuté très souvent, il peut déclencher de nombreux calculs et bloquer le thread principal du navigateur.

Dans cet article, vous allez apprendre à contrôler et à optimiser le mécanisme de détection des modifications en ignorant certaines parties de votre application et en n'exécutant la détection des modifications que lorsque cela est nécessaire.

Dans la détection des modifications d'Angular

Pour comprendre le fonctionnement de la détection des modifications d'Angular, examinons une application exemple.

Vous trouverez le code de l'application dans ce dépôt GitHub.

L'application répertorie les employés des deux services d'une entreprise (les ventes et la recherche et développement), et comporte deux composants:

  • AppComponent, qui est le composant racine de l'application, et
  • Deux instances de EmployeeListComponent, l'une pour les ventes et l'autre pour la recherche et le développement.

Exemple d'application

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>

Pour chaque employé, il y a un nom et une valeur numérique. L'application transmet la valeur numérique de l'employé à un calcul commercial et affiche le résultat à l'écran.

Examinez à présent 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 en tant qu'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 d'activité.

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 parcourt 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 des composants suivante:

Arborescence des composants

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.) représentant les différents employés du service.

Lorsque l'utilisateur commence à saisir le nom d'un nouvel employé dans la zone de saisie d'un élément EmployeeListComponent, Angular déclenche la détection des modifications pour l'ensemble de l'arborescence des composants à partir du 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'il n'a pas changé depuis la dernière vérification.

Pour voir à quel point cela peut être lent, ouvrez la version non optimisée du projet sur StackBlitz et essayez de saisir un nom d'employé.

Pour vérifier que le ralentissement provient de la fonction fibonacci, configurez l'exemple de projet et ouvrez l'onglet Performances des outils pour les développeurs Chrome.

  1. Appuyez sur Ctrl+Maj+J (ou Cmd+Option+J sur Mac) pour ouvrir les outils de développement.
  2. Cliquez sur l'onglet Performances.

Cliquez sur Record (Enregistrer) (en haut à gauche du panneau Performance) et commencez à saisir du texte dans l'une des zones de texte de l'application. Dans quelques secondes, cliquez à nouveau sur Record (Enregistrer) pour arrêter l'enregistrement. Une fois que les Outils pour les développeurs Chrome ont traité toutes les données de profilage collectées, le résultat qui s'affiche ressemble à ceci:

Profilage des performances

Si la liste comporte de nombreux employés, ce processus peut bloquer le thread UI du navigateur et provoquer des pertes de frames, ce qui nuit à l'expérience utilisateur.

Ignorer les sous-arborescences de composants

Lorsque l'utilisateur saisit du texte pour le EmployeeListComponent sales (ventes), 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 de recherche et développement 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:

Détection de modifications dans une sous-arborescence de composants

Pour consulter cette optimisation appliquée à l'application d'origine, cliquez ici.

Pour en savoir plus sur la stratégie de détection des modifications de OnPush, consultez la documentation officielle d'Angular.

Pour voir l'effet de cette optimisation, saisissez un nouvel employé dans la candidature sur StackBlitz.

Utiliser des pipes

Bien que la stratégie de détection des modifications pour EmployeeListComponent soit 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 du texte dans la saisie de texte correspondante.

Pour améliorer ce comportement, vous pouvez utiliser des pipes simples. Les pipes pures et impures acceptent des 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 recalcule son résultat que s'il reçoit une entrée différente de son appel précédent.

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 pipeline simple, Angular ne recalcule l'expression du pipe que lorsque ses arguments changent. Le framework déterminera si les arguments du pipe ont changé en effectuant une vérification des références. Angular n'effectuera donc aucun recalcul, sauf si la valeur numérique d'un employé est mise à jour.

Voici comment déplacer le calcul d'activité vers un pipe 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 de votre part.

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 associé à un service, l'application ne recalcule pas la valeur numérique pour chaque employé.

Dans l'application ci-dessous, vous pouvez voir à quel point la saisie est plus fluide.

Pour voir l'effet de la dernière optimisation, essayez cet exemple sur StackBlitz.

Le code permettant d'optimiser uniquement les pipelines de l'application d'origine est disponible ici.

Conclusion

Lorsque vous rencontrez des ralentissements d'exécution dans une application Angular:

  1. Profilez l'application à l'aide des outils pour les développeurs Chrome afin de déterminer l'origine des ralentissements.
  2. Introduction de la stratégie de détection des modifications OnPush pour élaguer les sous-arborescences d'un composant.
  3. Déplacez les calculs lourds vers des pipes pur pour permettre au framework d'effectuer la mise en cache des valeurs calculées.