quentinpugeat.fr

Comment faire face à son application Angular quand elle gonfle à vue d'œil ?

Post publié le 16/07/2024 20:57 par Quentin Pugeat

Optimiser son application Angular est moins difficile qu'en apparence ! Je suis passé par ce moment difficile, alors si vous aussi, voici quelques lignes pour vous montrer le chemin.

Un des plus gros projets sur lesquels j'ai pu travailler dans ma vie de développeur est une application Angular, dont l'ambition est de remplacer le logiciel de gestion natif et de transformer en profondeur les habitudes de travail de collaborateurs qui, à ma grande stupéfaction, utilisaient encore du papier pour absolument tout, chassant de facto toute opportunité d'automatisation et d'optimisation des communications internes à l'entreprise.

Cette application m'offre régulièrement l'opportunité d'apprendre et d'approfondir mes connaissances à propos du framework Angular, dont les capacités et les fonctionnalités continuent de me surprendre, même 3 ans après le début du projet. Il est à mes yeux l'un des frameworks qui a le plus et le mieux évolué depuis que l'on parle sérieusement de PWA.

Mais lorsque l'on utilise Angular de la manière la plus basique sur un projet d'une grande ampleur, on se retrouve très facilement face à des problèmes de performances qui peuvent sembler inéluctables et difficiles à contourner. Je suis passé par là. Plus l'application gagne en fonctionnalités, plus les utilisateurs la nourrissent en données, et plus ses tâches deviennent lourdes et coûteuses en ressources.

C'est à ce moment précis que la question de l'optimisation du projet est venue, et j'étais loin, très loin de me douter de toutes les possibilités qu'Angular offre pour optimiser un projet et le rendre performant comme au premier jour, tout en conservant sa complexité et ses possibilités.

Alors, j'ai décidé d'écrire ce billet de blog, pour tout-e-s les développeur-euse-s débutant-e-s et confirmé-e-s qui découvrent Angular et qui se retrouveront dans la position dans laquelle j'étais il y a un an. Bonne lecture !

Chassez les préjugés fatalistes concernant les applications Web

Quelques mots-clés sur Google ou un petit passage dans n'importe quel forum suffiront à comprendre de quoi je parle : les applications Web souffrent de beaucoup de préjugés concernant leurs mauvaises performances. Il est vrai que les navigateurs sont très gourmands en ressources matérielles et qu'il est vraiment très (trop) facile de se planter et de produire du code inefficace et coûteux en charge CPU. Il est aussi vrai que les mauvais exemples ne manquent pas, le framework Electron ayant largement contribué à cette situation.

Mais il est très important et même vital pour l'avenir et la crédibilité du Web de comprendre et d'apprendre que les technologies du Web sont tout à fait capables d'être légères et optimisées, pour peu que l'on se donne la peine d'écrire les lignes de code requises (et de supprimer celles qui nous encombrent, ce qui selon moi est encore plus difficile !).

Mettez à jour et nettoyez vos dépendances !

Les projets Node.js sont très volumineux. Tellement volumineux que la communauté des développeurs en fait des memes très régulièrement. On ne peut pas éviter cela, surtout s'agissant d'Angular qui embarque beaucoup de paquets avec lui.

Les objets les plus lourds de l'univers. Le soleil, une étoile à neutrons, un trou noir, le répertoire node_modules.

Ce que je recommande, surtout si vous avez décidé d'utiliser un template pour votre projet, c'est de vérifier que chacun des modules utilisés dans votre projet est vraiment utilisé quelque part. À ma grande surprise, j'ai réussi à supprimer une dizaine de paquets la dernière fois que j'ai fait cela !

Ensuite, mettez à jour votre application. Angular change pour le mieux, et la version 17 a apporté beaucoup d'optimisations et une nouvelle syntaxe de flux de contrôle qui facilite l'optimisation des boucles et l'usage du lazy loading, dont je parlerai plus tard dans ce billet.

Allégez la charge liée au rendu du DOM

La plus grande force d'Angular, c'est son mécanisme de détection des changements. C'est ce mécanisme qui permet à vos composants d'être toujours à jour à l'écran, à chaque fois qu'une variable change de valeur, ou à chaque fois qu'un évènement est traité. C'est grâce à ce mécanisme que vous pouvez "oublier" d'actualiser le DOM à chaque coin de fonction.

Mais une chose que les développeurs Web savent ou doivent savoir, c'est que le rendu du DOM est une opération très énergivore et lourde pour le processeur d'un appareil. C'est une opération si lourde qu'elle peut bloquer un navigateur si elle est enclenchée trop fréquemment, ou si les changements sont trop lourds à rendre.

Alors, si le composant que vous écrivez est particulièrement complexe (s'il contient des tableaux, des boucles, des tooltips, des menus, des éléments qui se chevauchent, ou si son contenu change fréquemment), il y a de grandes chances que votre application Angular se mette à ramer voire à planter.

Mais ne paniquez pas, car le mécanisme de détection des changements d'Angular propose des moyens d'alléger son fonctionnement, voire de le contourner.

La stratégie OnPush

Dans Angular, chaque composant déclare sa stratégie de détection des changements. La stratégie par défaut, ou CheckAlways, provoque la mise à jour du DOM du composant et de tous ses descendants à chaque fois qu'Angular traite un évènement dans l'arbre de composants.

Vous pouvez demander à Angular de sauter des parties de l'arbre de composants en changeant la stratégie d'un composant pour OnPush. Lors du traitement d'un évènement, Angular ne vérifie pas les changements des descendants du composant qui ont OnPush ni des descendants de deux-ci. Vous pouvez ensuite décider de provoquer la mise à jour du composant manuellement avec ChangeDetectionRef.markForCheck().

La documentation d'Angular détaille les cas de figure. Choisissez OnPush pour les composants dont le rafraîchissement fréquent n'est pas nécessaire ou pourrait être coûteux en ressources.

import { ChangeDetectionStrategy, Component } from '@angular/core';

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyComponent {
  // ...
}

Effectuez les tâches les plus coûteuses en dehors de la NgZone

Angular a recours à Zone.js pour détecter les changements d'état de l'application, lors d'évènements asynchrones comme setTimeout(), les requêtes sur le réseau et les évènements. Il est possible de demander à Angular d'effectuer certaines tâches en dehors de la NgZone, les rendant ainsi complètement invisibles.

Utilisez ceci lorsque vous voulez effectuer des tâches n'ont pas d'impact sur l'affichage. La documentation d'Angular précise quand et comment le faire.

Optimisez les boucles for de vos templates

Dans une application Web, il peut devenir très courant d'utiliser des boucles alimentées par des collections d'objets pour générer des listes ou des tableaux. Ce genre d'opération est très coûteuse lorsque les collections sont larges ou les objets complexes. De plus, un uniquement changement dans la collection entraîne le rafraîchissement de l'entièreté de la structure DOM, ce qui peut planter l'application.

Angular offre un mécanisme appelé trackBy qui permet d'indiquer l'identifiant d'un objet dans une collection. Ainsi, Angular peut mieux déterminer si le rafraîchissement de l'objet dans le DOM est nécessaire. trackBy nécessite une TrackByFunction qui retourne pour un élément de collection, son identifiant.

public myTrackFn(index: number, item: T): any {
  return item.id;
}
<element *ngFor="let item of collection; trackBy: myTrackFn">
  <!-- ... -->
</element>

Dans Angular 17.0 et supérieur, les boucles @for simplifient ce processus en permettant simplement d'indiquer l'identifiant de l'élément après le mot-clé track.

// Indiquez comment identifier item dans collection,
// ou $index pour les identifier par leur index dans la collection.
@for(item of collection; track item.id) {
  {{ item.name }}
}

Le lazy loading à la rescousse

Depuis Angular 17, le flux de contrôle permet de différer l'affichage d'une partie du template du DOM jusqu'à la satisfaction d'une condition. Cela permet, par exemple, de ne charger le template d'un composant que lorsqu'il est réellement prêt (après un appel HTTP, après une animation de transition, etc).

Utilisez la structure @defer pour encapsuler une partie d'une composant dont il faut différer le rendu. Le mot clé prend un paramètre facultatif. Lorsqu'il n'est pas fourni, le rendu est différé jusqu'à ce que le navigateur soit idle. Il est possible de différer avec une condition propre, un timer, l'emplacement du scrolling, une interaction, et d'autres conditions.

Aussi, il est possible de définir un contenu temporaire qui s'affiche à la place du bloc différé le temps que son chargement ait lieu.

@defer {
  <large-component />
} @placeholder {
  <p>Chargement en cours...</p>
}

La documentation d'Angular explique tout à propos des vues différables.

Réduisez le nombre d'appels HTTP

Ceci n'a pas de rapport direct avec Angular, mais c'est toujours bon à prendre. Toute application Web qui prend du volume aura aussi tendance à envoyer un plus grand nombre de requêtes, ou des requêtes volumineuses. Le temps d'attente occupé par ces requêtes peuvent devenir irritantes, et occuper votre serveur inutilement.

Si votre application Angular est une PWA, vous pouvez activer la mise en cache de tout ou partie d'une API (ou plutôt de ses retours). Utilisez dataGroups dans le fichier ngsw-config.json comme indique dans la documentation d'Angular.

Pour conclure

L'optimisation d'une application Angular peut nécessiter quelques pirouettes intellectuelles, mais elle est à portée de n'importe quel développeur avec quelques mots-clés et quelques procédures faciles à reproduire et à intégrer à ses habitudes. Un rendu de DOM moins agressif, des boucles moins lourdes à rendre, des affichages différés : les solutions ne manquent pas pour faire de votre projet Angular une véritable application défiant toute concurrence.

Alors, tordez le cou aux préjugés qui prétendent que les PWA ne seront jamais assez optimisées, et montrez à tous de quel bois vous vous chauffez !