Les développeurs angulaires doivent beaucoup à zone.js. Elle, par exemple, aide à atteindre une facilité presque magique en travaillant avec Angular. En fait, presque toujours, lorsque vous avez juste besoin de changer une propriété, et que nous la changeons sans penser à rien, Angular restitue les composants correspondants. Par conséquent, ce que l'utilisateur voit contient toujours les dernières informations. C'est tout simplement génial.
Ici, je voudrais explorer certains aspects de la façon dont l'utilisation du nouveau compilateur Ivy (qui est apparu dans Angular 9) peut grandement faciliter le rejet de l'utilisation de zone.js.

En abandonnant cette bibliothèque, j'ai pu augmenter considérablement les performances de l'application Angular fonctionnant sous une charge importante. Dans le même temps, j'ai réussi à implémenter les mécanismes dont j'avais besoin en utilisant les décorateurs TypeScript, ce qui a conduit à très peu de ressources système supplémentaires.
Veuillez noter que l'approche d'optimisation des applications angulaires, présentée dans cet article, n'est possible que parce que Angular Ivy et AOT sont activés par défaut. Cet article est écrit à des fins pédagogiques, il ne vise pas à promouvoir l'approche qui y est présentée au développement de projets Angular.
Pourquoi pourriez-vous avoir besoin d'utiliser Angular sans zone.js?
Avant de continuer, posons une question importante: «Vaut-il la peine de se débarrasser de zone.js, étant donné que cette bibliothèque nous aide à restituer les modèles avec peu d'effort?» Bien sûr, cette bibliothèque est très utile. Mais, comme d'habitude, vous devez tout payer.
Si votre application a des exigences de performances spécifiques, la désactivation de zone.js peut aider à répondre à ces exigences. Un exemple d'application dans laquelle les performances sont cruciales est un projet dont l'interface est mise à jour très souvent. Dans mon cas, un tel projet s'est avéré être une application de trading en temps réel. Sa partie client reçoit en permanence des messages via le protocole WebSocket. Les données de ces messages doivent être affichées le plus rapidement possible.
Supprimer zone.js d'Angular
Angular peut très facilement fonctionner sans zone.js. Pour ce faire, vous devez d'abord mettre en commentaire ou supprimer la commande d'importation correspondante, qui se trouve dans le fichier
polyfills.ts
.
Commande d'importation zone.js mise en commentaireEnsuite - vous devez équiper le module racine avec les options suivantes:
platformBrowserDynamic() .bootstrapModule(AppModule, { ngZone: 'noop' }) .catch(err => console.error(err));
Lierre angulaire: changements d'auto-détection avec ɵdetectChanges et ɵmarkDirty
Avant de commencer à créer un décorateur TypeScript, nous devons savoir comment Ivy vous permet d'appeler le processus de détection des modifications de composants, de le rendre sale et de contourner zone.js et DI.
Deux fonctions supplémentaires sont maintenant à notre disposition, exportées de
@angular/core
. Ce sont
ɵdetectChanges
et
ɵmarkDirty
. Ces deux fonctions sont toujours destinées à un usage interne et sont instables - le symbole
ɵ
est situé au début de leurs noms.
Voyons comment utiliser ces fonctionnalités.
▍ Fonction DmarkDirty
Cette fonction vous permet de marquer un composant, le rendant «sale», c'est-à-dire ayant besoin d'un nouveau rendu. Si le composant n'était pas marqué «sale» avant son appel, elle prévoit de démarrer le processus de détection des modifications.
import { ɵmarkDirty as markDirty } from '@angular/core'; @Component({...}) class MyComponent { setTitle(title: string) { this.title = title; markDirty(this); } }
▍ ɵDetectChanges, fonction
La documentation interne angulaire indique que, pour des raisons de performances, vous ne devez pas utiliser
ɵdetectChanges
. Au lieu de cela, il est recommandé d'utiliser la fonction
ɵmarkDirty
. La fonction
ɵdetectChanges
invoque de manière synchrone le processus de détection des changements dans un composant et ses sous-composants.
import { ɵdetectChanges as detectChanges } from '@angular/core'; @Component({...}) class MyComponent { setTitle(title: string) { this.title = title; detectChanges(this); } }
Détecter automatiquement les modifications à l'aide du décorateur TypeScript
Bien que les fonctionnalités fournies par Angular augmentent l'utilisabilité du développement en laissant le DI tourner, le programmeur peut toujours être frustré par le fait qu'il doit importer et appeler ces fonctions lui-même pour démarrer le processus de détection des modifications.
Afin de simplifier le démarrage automatique de la détection des modifications, vous pouvez écrire un décorateur TypeScript, qui résoudra indépendamment ce problème. Bien sûr, il y a quelques limites ici, dont nous discuterons ci-dessous, mais dans mon cas, cette approche s'est avérée être exactement ce dont j'avais besoin.
▍ Présentation du décorateur @observed
Afin de détecter les changements, en faisant le moins d'efforts possible, nous allons créer un décorateur qui peut être appliqué de trois façons. À savoir, il s'applique aux entités suivantes:
- Aux méthodes synchrones.
- Objets observables.
- Aux objets ordinaires.
Prenons quelques petits exemples. Dans le fragment de code suivant, nous appliquons le décorateur
@observed
à l'objet d'
state
et à la méthode
changeTitle
:
export class Component { title = ''; @observed() state = { name: '' }; @observed() changeTitle(title: string) { this.title = title; } changeName(name: string) { this.state.name = name; } }
- Pour vérifier les modifications apportées à l'objet d'
state
, nous utilisons un objet proxy qui intercepte les modifications apportées à l'objet et appelle la procédure de détection des modifications. - Nous
changeTitle
méthode changeTitle
en appliquant une fonction qui appelle d'abord cette méthode, puis démarre le processus de détection des modifications.
Et voici un exemple avec
BehaviorSubject
:
export class AppComponent { @observed() show$ = new BehaviorSubject(true); toggle() { this.show$.next(!this.show$.value); } }
Dans le cas d'objets observables, l'utilisation d'un décorateur semble un peu plus compliquée. À savoir, vous devez vous abonner à l'objet observé et marquer le composant comme «sale» dans l'abonnement, mais vous devez également effacer l'abonnement. Pour ce faire, nous réaffectons
ngOnInit
et
ngOnDestroy
pour vous abonner et le nettoyer plus tard.
▍Création d'un décorateur
Voici la signature décoratrice
observed
:
export function observed() { return function( target: object, propertyKey: string, descriptor?: PropertyDescriptor ) {} }
Comme vous pouvez le voir, le
descriptor
est un paramètre facultatif. En effet, nous avons besoin que le décorateur soit appliqué aux méthodes et aux propriétés. Si le paramètre existe, cela signifie que le décorateur est appliqué à la méthode. Dans ce cas, nous faisons ceci:
Ensuite, vous devez vérifier de quel type de propriété il s'agit. Il peut s'agir d'un objet observable ou d'un objet ordinaire. Ici, nous utiliserons une autre API angulaire interne. Il n'est, je crois, pas destiné à être utilisé dans des applications régulières (désolé!).
Nous parlons de la propriété
ɵcmp
, qui donne accès aux propriétés traitées par Angular après leur définition. Nous pouvons les utiliser pour remplacer les méthodes des
onDestroy
onInit
et
onDestroy
.
const getCmp = type => (type).ɵcmp; const cmp = getCmp(target.constructor); const onInit = cmp.onInit || noop; const onDestroy = cmp.onDestroy || noop;
Afin de marquer une propriété comme une propriété à surveiller, nous utilisons
ReflectMetadata
et définissons sa valeur sur
true
. Par conséquent, nous saurons que nous devons observer la propriété lorsque le composant est initialisé:
Reflect.set(target, propertyKey, true);
Il est maintenant temps de remplacer le crochet
onInit
et de vérifier les propriétés lors de la création de l'instance de composant:
cmp.onInit = function() { checkComponentProperties(this); onInit.call(this); };
Nous définissons la fonction
checkComponentProperties
, qui contournera les propriétés du composant, en les filtrant en fonction de la valeur définie précédemment à l'aide de
Reflect.set
:
const checkComponentProperties = (ctx) => { const props = Object.getOwnPropertyNames(ctx); props.map((prop) => { return Reflect.get(target, prop); }).filter(Boolean).forEach(() => { checkProperty.call(ctx, propertyKey); }); };
La fonction
checkProperty
sera responsable de la décoration des propriétés individuelles. Tout d'abord, nous vérifions si la propriété est un objet observable ou régulier. S'il s'agit d'un objet observable, nous y souscrivons et ajoutons l'abonnement à la liste des abonnements stockés dans le composant pour ses besoins internes.
const checkProperty = function(name: string) { const ctx = this; if (ctx[name] instanceof Observable) { const subscriptions = getSubscriptions(ctx); subscriptions.add(ctx[name].subscribe(() => { markDirty(ctx); })); } else {
Si la propriété est un objet ordinaire, nous la convertirons en objet Proxy et appellerons
markDirty
dans sa fonction de
handler
:
const handler = { set(obj, prop, value) { obj[prop] = value; ɵmarkDirty(ctx); return true; } }; ctx[name] = new Proxy(ctx, handler);
Enfin, vous devez effacer l'abonnement après avoir détruit le composant:
cmp.onDestroy = function() { const ctx = this; if (ctx[subscriptionsSymbol]) { ctx[subscriptionsSymbol].unsubscribe(); } onDestroy.call(ctx); };
Les possibilités de ce décorateur ne peuvent être qualifiées de complètes. Ils ne couvrent pas toutes les utilisations possibles qui peuvent apparaître dans une grande application. Par exemple, ce sont des appels à des fonctions de modèle qui retournent des objets observables. Mais j'y travaille.
Malgré cela, le décorateur ci-dessus suffit pour mon petit projet. Vous trouverez son code complet à la fin du document.
Analyse des résultats d'accélération des applications
Maintenant que nous avons parlé un peu des mécanismes internes d'Ivy et de la façon de créer un décorateur en utilisant ces mécanismes, il est temps de tester ce que nous avons dans une vraie application.
Pour découvrir l'effet de la suppression de zone.js sur les performances des applications angulaires, j'ai utilisé mon projet de loisir
Cryptofolio .
J'ai appliqué le décorateur à tous les liens nécessaires utilisés dans les modèles et désactivé zone.js. Par exemple, considérez le composant suivant:
@Component({...}) export class AssetPricerComponent { @observed() price$: Observable<string>; @observed() trend$: Observable<Trend>;
Deux variables sont utilisées dans le modèle: le
price
(le
price
l'actif sera situé ici) et la
trend
(cette variable peut prendre des valeurs à la
up
, à la
up
et à la
down
, indiquant la direction du changement de prix). Je les ai décorés avec
@observed
.
Size Taille du bundle de projet
Pour commencer, regardons à quel point la taille du bundle de projet a diminué tout en se débarrassant de zone.js. Voici le résultat de la construction du projet avec zone.js.
Résultat de la construction d'un projet avec zone.jsEt voici le montage sans zone.js.
Le résultat de la construction d'un projet sans zone.jsFaites attention au
polyfills-es2015.xxx.js
. Si le projet utilise zone.js, sa taille est d'environ 35 Ko. Mais sans zone.js - seulement 130 octets.
OotingDémarrage
J'ai recherché deux options d'application en utilisant Lighthouse. Les résultats de cette étude sont donnés ci-dessous. Il convient de noter que je ne les prendrais pas trop au sérieux. Le fait est qu'en essayant de trouver les valeurs moyennes, j'ai obtenu des résultats significativement différents en effectuant plusieurs mesures pour la même version d'application.
Peut-être que la différence dans l'évaluation des deux options d'application ne dépend que de la taille des bundles.
Voici donc le résultat obtenu pour une application qui utilise zone.js.
Résultats d'analyse pour une application qui utilise zone.jsEt voici ce qui s'est passé après avoir analysé l'application dans laquelle zone.js n'est pas utilisé.
Résultats d'analyse pour une application qui n'utilise pas zone.js▍ Performance
Et maintenant, nous sommes arrivés au plus intéressant. Il s'agit des performances d'une application exécutée sous charge. Nous voulons savoir comment le processeur se sent lorsque l'application affiche des mises à jour de prix pour des centaines d'actifs plusieurs fois par seconde.
Afin de charger l'application, j'ai créé 100 entités qui fournissent des données conditionnelles à des prix qui changent toutes les 250 ms. Si le prix augmente, il s'affiche en vert. Si réduit - rouge. Tout cela pourrait sérieusement charger mon MacBook Pro.
Il convient de noter qu'en travaillant dans le secteur financier sur plusieurs applications conçues pour la transmission à haute fréquence de fragments de données, j'ai rencontré à plusieurs reprises une situation similaire.
Pour analyser la façon dont différentes versions de l'application utilisent les ressources du processeur, j'ai utilisé les outils de développement Chrome.
Voici à quoi ressemble l'application qui utilise zone.js.
Charge système créée par une application qui utilise zone.jsEt voici comment fonctionne une application dans laquelle zone.js n'est pas utilisée.
Charge système créée par une application qui n'utilise pas zone.jsNous analysons ces résultats en faisant attention au graphique de charge du processeur (jaune):
- Comme vous pouvez le voir, une application qui utilise zone.js charge constamment le processeur de 70 à 100%! Si vous maintenez l'onglet du navigateur ouvert pendant longtemps, créant une telle charge sur le système, l'application qui s'exécute peut bien échouer.
- Et la version de l'application où zone.js n'est pas utilisée crée une charge stable sur le processeur dans la plage de 30 à 40%. Super!
Veuillez noter que ces résultats ont été obtenus avec la fenêtre Chrome Developer Tools ouverte, ce qui met également à rude épreuve le système et ralentit l'application.
▍ augmentation de la charge
J'ai essayé de m'assurer que chaque entité responsable de la mise à jour du prix émettrait 4 mises à jour supplémentaires chaque seconde en plus de ce qu'elle produit déjà.
Voici ce que nous avons réussi à découvrir sur l'application dans laquelle zone.js n'est pas utilisé:
- Cette application faisait normalement face à la charge, utilisant maintenant environ 50% des ressources du processeur.
- Il a réussi à charger le processeur autant que l'application avec zone.js, uniquement lorsque les prix étaient mis à jour toutes les 10 ms (les nouvelles données, comme auparavant, provenaient de 100 entités).
▍ Analyse des performances avec Angular Benchpress
L'analyse des performances que j'ai menée ci-dessus ne peut pas être qualifiée de particulièrement scientifique. Pour une étude plus sérieuse des performances de différents frameworks, je recommanderais d'utiliser
ce benchmark . Pour la recherche, Angular doit choisir la version habituelle de ce framework et sa version sans zone.js.
Moi, inspiré par quelques idées de ce benchmark, j'ai créé un
projet qui effectue des calculs lourds. J'ai testé ses performances avec
Angular Benchpress .
Voici le code du composant testé:
@Component({...}) export class AppComponent { public data = []; @observed() run(length: number) { this.clear(); this.buildData(length); } @observed() append(length: number) { this.buildData(length); } @observed() removeAll() { this.clear(); } @observed() remove(item) { for (let i = 0, l = this.data.length; i < l; i++) { if (this.data[i].id === item.id) { this.data.splice(i, 1); break; } } } trackById(item) { return item.id; } private clear() { this.data = []; } private buildData(length: number) { const start = this.data.length; const end = start + length; for (let n = start; n <= end; n++) { this.data.push({ id: n, label: Math.random() }); } } }
J'ai lancé un petit ensemble de références en utilisant Protractor et Benchpress. Les opérations ont été effectuées un nombre spécifié de fois.
Benchpress en actionRésultats
Voici un échantillon des résultats obtenus avec Benchpress.
Résultats BenchpressVoici une explication des indicateurs présentés dans ce tableau:
gcAmount
: volume des opérations gc (garbage collection), Kb.gcTime
: temps de fonctionnement gc, ms.majorGcTime
: heure des principales opérations gc, ms.pureScriptTime
: temps d'exécution du script en ms, hors opérations gc et rendu.renderTime
: temps de rendu, ms.scriptTime
: temps d'exécution du script prenant en compte les opérations gc et le rendu.
Nous allons maintenant considérer l'analyse des performances de certaines opérations dans différentes variantes d'application. Le vert montre les résultats d'une application qui utilise zone.js, l'orange montre les résultats d'une application sans zone.js. Veuillez noter que seul le temps de rendu est analysé ici. Si vous êtes intéressé par tous les résultats des tests, cochez
ici .
Test: création de 1000 lignes
Dans le premier test, 1000 lignes sont créées.
Résultats des testsTest: création de 10 000 lignes
À mesure que la charge sur les applications augmente, la différence de leurs performances augmente également.
Résultats des testsTest: rejoignez 1000 lignes
Dans ce test, 1 000 lignes sont ajoutées à 10 000 lignes.
Résultats des testsTest: suppression de 10 000 lignes
Ici, 10 000 lignes sont créées, qui sont ensuite supprimées.
Résultats des testsCode source de TypeScript Decorator
Ci-dessous se trouve le code source du décorateur TypeScript discuté ici. Ce code peut également être trouvé
ici .
Résumé
Bien que j'espère que vous avez aimé mon histoire sur l'optimisation des performances des projets Angular, j'espère également que je ne vous inclinerai pas à vous précipiter pour supprimer zone.js de votre projet. La stratégie décrite ici doit être le tout dernier recours auquel vous pouvez recourir pour augmenter les performances de votre application Angular.
Vous devez d'abord essayer des approches telles que l'utilisation de la stratégie de détection des modifications OnPush, l'application de
trackBy
, la désactivation des composants, l'exécution de code en dehors de zone.js, la liste noire des événements zone.js (cette liste de méthodes d'optimisation peut être poursuivie). L'approche présentée ici est assez chère, et je ne suis pas sûr que tout le monde soit prêt à payer un prix aussi élevé pour ses performances.
En fait, le développement sans zone.js n'est peut-être pas la chose la plus attrayante. Ce n'est peut-être pas seulement pour la personne impliquée dans le projet, qui est sous son contrôle total. C'est-à-dire qu'il est propriétaire de dépendances et a la capacité et le temps de tout mettre sous sa forme appropriée.
S'il s'avère que vous avez tout essayé et que le goulot d'étranglement de votre projet est précisément zone.js, alors vous devriez peut-être essayer d'accélérer Angular en détectant indépendamment les changements.
J'espère que cet article vous a permis de voir ce que Angular attend à l'avenir, de quoi Ivy est capable et ce que zone.js peut faire pour maximiser la vitesse d'application.
Chers lecteurs! Comment optimisez-vous vos projets Angular qui nécessitent des performances maximales?
