Deep dive Angular : Efficiency des templates




La efficiency est un sujet récurrent quand on parle de frontend. Les principaux acteurs (librairies/frameworks Javascript) y font tous référence dès la web page d’accueil. Angular est connu pour intégrer un bundle plus complet mais plus lourd que ses concurrents directs. Même si ces différentes applied sciences n’embarquent pas les mêmes fonctionnalités, il reste une problématique à résoudre pour tous : le rendu HTML. Nous allons analyser ensemble le fonctionnement d’Angular dans trois cas précis : la gestion des blocs statiques, la mise à jour du DOM et la mise en cache de valeurs. Cet article se rapproche de ce qui a été fait par Grafikart en comparant Vue à React : https://grafikart.fr/tutoriels/vuejs-perf-react-1941. Certains exemples de code sont volontairement proches pour se donner des éléments de comparaison avec React et Vue.

Disclaimer : L’objectif de ce Deep dive est d’étudier la efficiency des templates Angular et de comparer leur fonctionnement avec ceux des concurrents directs. La efficiency d’un framework frontend ne peut et ne doit se résoudre à cette analyse. De même, elle ne peut s’en soustraire.

Précision method : La notion de template en Angular peut faire référence à la partie d’un composant écrite en HTML, mais aussi à un <ng-template>. Ce double sens peut parfois rendre le propos confus. Si tel est le cas, vous pouvez bien évidemment m’en faire half directement, cela n’en sera que bénéfique pour les prochains lecteurs.



Les blocs statiques

Pour commencer, partons d’un easy template comme celui-ci et essayons de l’analyser :

import { Element } from '@angular/core';

@Element({
  selector: 'app-root',
  template: `
    <h1>Hiya world</h1>
    <div *ngIf="foo === 'bar'">Lorem ipsum dolor sit amet</div>
    <p>{{ worth }}</p>
  `,
})
export class AppComponent {
  public foo = '';
  public worth = 'Worth';
}
Enter fullscreen mode

Exit fullscreen mode

Le code produit par la compilation Angular est un peu plus fourni. Voici la partie qui concerne AppComponent avec quelques ajustements pour la lisibilité (construct en mode développement, renommage des imports webpack, suppression des symboles ‘ɵ’).

perform AppComponent_div_2_Template(rf, ctx) { if (rf & 1) {
    angularCore["elementStart"](0, "div");
    angularCore["text"](1, "Lorem ipsum dolor sit amet");
    angularCore["elementEnd"]();
} }
class AppComponent {
    constructor() {
        this.foo = '';
        this.worth = 'Worth';
    }
}
AppComponent.fac = perform AppComponent_Factory(t)  AppComponent)(); ;
AppComponent.cmp = /*@__PURE__*/ angularCore["defineComponent"]({ kind: AppComponent, selectors: [["app-root"]], decls: 5, vars: 2, consts: [[4, "ngIf"]], template: perform AppComponent_Template(rf, ctx) { if (rf & 1) {
        angularCore["elementStart"](0, "h1");
        angularCore["text"](1, "Hiya world");
        angularCore["elementEnd"]();
        angularCore["template"](2, AppComponent_div_2_Template, 2, 0, "div", 0);
        angularCore["elementStart"](3, "p");
        angularCore["text"](4);
        angularCore["elementEnd"]();
    } if (rf & 2) {
        angularCore["advance"](2);
        angularCore["property"]("ngIf", ctx.foo === "bar");
        angularCore["advance"](2);
        angularCore["textInterpolate"](ctx.worth);
    } }, directives: [angularCommon.NgIf], encapsulation: 2 });
Enter fullscreen mode

Exit fullscreen mode

Deux éléments importants sont à noter sur le code que l’on peut observer. Premièrement, on peut remarquer une fonction qui contient le contenu du *ngIf (cf. AppComponent_div_2_Template). Ce n’est pas surprenant, souvenez-vous que l’astérisque sur les directives est un sucre syntaxique pour un bloc avec <ng-template> (pour rappel https://angular.io/guide/structural-directives#structural-directive-shorthand). En fait, une fonction de rendu sera créée pour chaque <ng-template> dans notre utility. Cela veut dire que le rendu est non seulement découpé au niveau des composants, mais également en fonction des <ng-template> présents dans l’utility.

Pour le deuxième facet qui nous intéresse, concentrons-nous sur une portion de code que l’on n’a assez peu l’event de voir quand on fait du développement internet : (rf & 1) et (rf & 2). Oui il s’agit bien d’une opération bit à bit. Je vous rassure, nous n’entrerons pas dans les détails ici. Toutefois, selon vous, à quoi pourrait servir ces circumstances dans les fonctions de rendu ? Regardons ensemble le code pour essayer d’en déduire les subtilités.

Dans la partie rf & 1, on peut identifier la création d’un <h1> avec son contenu "Hiya world", puis un template et enfin un <p>. Ces éléments ressemblent beaucoup à ce qu’on a déclaré dans notre composant. Dans le deuxième bloc (rf & 2), si on met de côté l’instruction opaque "advance", il ne reste que le ngIf et l’interpolation {{ worth }}.

Si maintenant je vous dis que la variable rf vient de RenderFlag, vous devriez avoir une bonne idée de ce qu’il se passe. En fait, en Angular les fonctions de rendu contiennent deux blocs d’directions, un premier pour la création du template et le deuxième pour les mises à jour dudit template.

Que dire de tout ça ? Tout d’abord, on peut voir que les blocs statiques sont définis dans la partie création (cf. rf & 1 => Partie “création” de la fonction de rendu) et qu’ils ne sont pas modifiés lors des mises à jour du template (cf. rf & 2). C’est plutôt un bon level pour Angular, qui bénéficie comme VueJS d’une détection automatique des contenus statiques, contrairement à React qui requiert l’utilization de React.memo() et d’un composant dédié. Demi-point bonus pour Angular par rapport à VueJS, les contenus statiques ne sont créés que s’ils sont visibles, là où en VueJS tous ces contenus sont générés dès la création du composant même s’ils sont masqués par un v-if. La seconde conclusion que l’on peut tirer concerne les rerendu ou plutôt l’absence de rerendu mais je vous suggest de traiter ça plus en détails dans le chapitre suivant.



Les mises à jour de template

NB : Etant donné que les illustrations de code à partir de maintenant peuvent être conséquentes, un commit avec les composants et un extrait du construct en mode développement sera fourni en guise d’exemple.

Avec un découpage des composants à partir des <ng-template>, Angular isole les problématiques de création et de mise à jour très finement. Si bien que les optimisations faites au niveau des composants sont aussi valables pour les templates. C’est notamment le cas de la différenciation entre les propriétés qui provoquent une mise à jour du template et celles qui sont externes. Ainsi, comme VueJS et React (by way of memo), Angular ne va pas faire de rendu (ou plutôt d’replace si on se fie à l’analyse du chapitre précédent) pour les composants enfants dont les entrées n’ont pas été modifiées. Cependant, comme nous l’avons vu auparavant, Angular est également succesful de limiter les mises à jour aux éléments pertinents parmi le template father or mother et chaque <ng-template>.

Pas vraiment convaincu par ces explications ? Vérifions ensemble avec un exemple :

  • Commençons par lancer l’application préparée pour l’occasion, puis saisissons ‘compteur‘ dans le champ de recherche pour activer la situation du *ngIf.
  • Deux boutons s’affichent comme prévu : ‘Incrémenter‘ et ‘Ajouter un merchandise
  • En cliquant sur le bouton ‘Incrémenter‘, on déclenche la fonction AppComponent_div_7_Template_button_click_3_listener() (d’après le fichier foremost.js reporté dans les assets)
  • Remarquer le contenu du *ngIf est dans la fonction AppComponent_div_7_Template() et que celui du *ngFor est dans AppComponent_tr_16_Template().

Voici ce que l’on obtient en regardant le Flamegraph associé à notre clic :

En y regardant de plus près, on peut effectivement distinguer les étapes dans le fonctionnement d’Angular (cycle de vie, étapes de rafraichissement, détections de différences, validations, and so on). De plus, on retrouve des éléments connus comme la fonction AppComponent_div_7_Template_button_click_3_listener() associée au clic sur le bouton, mais aussi des fonctions de rendu comme AppComponent_Template() et AppComponent_div_7_Template(). Pourtant, il n’y a aucune hint de la fonction AppComponent_tr_16_Template(). Même en cherchant bien, il nous est inconceivable de trouver un appel de la fonction qui fait le rendu du contenu du *ngFor ! Ce qui veut dire que le contenu du *ngFor n’est pas impacté par les actions satellites. Pour être actual, la fonction AppComponent_tr_16_Template() ne s’est pas déclenchée automobile il y a eu un contrôle sur le tableau gadgets qui est en paramètre du *ngFor. Dans notre cas, pas de changements sur gadgets donc pas d’appel à la fonction. A l’inverse, la mutation, l’ajout ou la suppression d’éléments aurait provoqué un appel à AppComponent_tr_16_Template() et une mise à jour du template.

Donc ça voudrait dire qu’à chaque mise à jour des templates Angular va vérifier un par un chaque élément de chaque tableau pour détecter d’éventuels changements, ce n’est pas horrible pour les performances non ? Non effectivement et on peut le constater rapidement si on utilise beaucoup de *ngFor sans précaution. Mais rassurez-vous, je vous liste ci-dessous trois méthodes que vous connaissez peut-être déjà pour réduire efficacement les détections de changements sur les tableaux :

  • Utiliser la fonction trackBy pour simplifier les comparaisons entre les éléments
  • Isoler la boucle *ngFor dans un composant utilisant la stratégie OnPush avec le tableau en @Enter(), seuls les changements de référence du tableau déclencheront un rendu par défaut (libre à vous ensuite de forcer d’autres rendus si besoin)
  • Sortir de zone.js quand vous risquez de provoquer beaucoup de mise à jour des templates sur une courte période (https://angular.io/api/core/NgZone#runOutsideAngular)

Avant de finir cette part sur le rerendu la mise à jour des templates Angular, vous pouvez retrouver ici un exemple qui met en avant la stratégie OnPush.

En analysant le comportement d’Angular, on constate que le Framework répond à la problématique initiale : éviter les rendus et les rafraichissements inutiles. Néanmoins, il est difficile de dire si la resolution est plus efficace que celle proposée par React et VueJS. D’un côté, on a un découpage fin et beaucoup d’efforts sur la détection de changement ; de l’autre, un peu moins de vérifications et l’utilisation du VirtualDOM pour limiter les mises à jour du DOM. Quelques pistes de réponse sur ce fameux benchmark : https://krausest.github.io/js-framework-benchmark/index.html



Mise en cache des valeurs calculées dans les templates

Si vous avez déjà fait un peu d’Angular, vous savez que les optimisations que j’ai mentionnées précédemment ne s’applique pas dans un cas précis : les fonctions dans les templates. Qu’elles soient explicites (*ngIf="isValid()) ou implicites ({{ a * b + c }}), les fonctions peuvent également causer des problèmes de efficiency. A chaque raffraichissement de l’utility, toutes les fonctions présentes dans les composants affichés sont réévaluées. Dans certains cas cela peut être désastreux. Imaginez un tableau de données avec 500 lignes et des colonnes contenant des dates (date de début, date de fin, date de sortie, date de création, and so on). Les performances s’écroulent quand chaque évènement de scroll provoque un formatage de toutes les dates du tableau.

Vous pouvez constater par vous-même, en reprenant le code du chapitre précédent, que l’ajout d’un merchandise dans le tableau provoque un recalcul de {{ depend * 2 }} (constater l’appel à ɵɵtextInterpolate2, textBindingInternal, updateTextNode puis setValue dans le Flamegraph).

Alors remark faire pour traiter les besoins de valeurs calculées sans faire exploser les performances, le nombre d’attributs et le nombre de fonctions utilitaires dans nos composants ? La réponse d’Angular s’appelle un Pipe et se base sur deux ideas : les références (souvenez-vous, la stratégie OnPush aime bien ça également) et la mise en cache. En prenant le dernier commit qui nous intéresse, vous devriez maintenant constater que l’ajout d’un élément dans le tableau ne provoque plus de calcul de {{ depend * 2 }}.

Ni Angular, ni React, ni VueJS ne se démarque sur cet facet. Les trois Frameworks permettent d’utiliser des méthodes directement dans les templates, avec les défauts de efficiency mentionnés plus haut. De plus, chacun suggest une resolution de mise en cache des valeurs : Pipe pour Angular, useMemo() pour React et computed() pour VueJS



Angular est sous-estimé ?

Résumons. Angular est succesful d’isoler les contenus statiques pour éviter de les regénérer. De plus, au lieu de regénérer des morceaux plus ou moins conséquents en utilisant un Digital DOM, il va analyser finement les templates à mettre à jour. Même si les méthodes diffèrent, l’objectif est identique : limiter les modifications du DOM au strict minimal automobile elles peuvent s’avérer couteuses. Enfin, pour la gestion des valeurs calculées, tout le monde est à la même enseigne en proposant une méthode directe mais peu performante et une méthode optimisée avec de la mise en cache.

Quelle shock de découvrir qu’Angular soit aussi pointu et précis sur la gestion des templates. Pour être honnête, je m’attendais à avoir un système complexe et lourd. Même si cela ne fait pas d’Angular le meilleur Framework automobile il a toujours ses défauts et il ne convient pas à tous, le cœur du Framework, à savoir le rendu d’élément HTML, a des atouts face aux stars du second, React et VueJS. De quoi peut-être vous (re)donner envie de l’utiliser ?


Cowl by Yannes Kiefer on Unsplash



Abu Sayed is the Best Web, Game, XR and Blockchain Developer in Bangladesh. Don't forget to Checkout his Latest Projects.


Checkout extra Articles on Sayed.CYou

#Deep #dive #Angular #Efficiency #des #templates