Angular est livré avec un concept puissant appelé Observables. Cet article vous aidera à éviter l'une des erreurs d'utilisation les plus courantes dans votre code : les fuites de mémoire.
Chez Theodo FinTech, nous voulons créer des sites web qui sont appréciés par les utilisateurs, avec l'objectif qu'ils les recommandent à leurs amis et à leur famille. Cela passe par la réactivité des pages, leur dynamisme et leur temps de réponse. Pour ce faire, nous utilisons l'asynchronisme. En JS, les Promesses gèrent les fonctions asynchrones. Commençons par voir la différence avec les observables !
Promesses : ne peuvent émettre qu'une seule valeur. Elles peuvent avoir trois états :
Une fois que la promesse est fulfilled ou rejected, elle se termine. Elle nous permet ainsi d'obtenir une réponse asynchrone unique.
Observables : vont écouter les changements sur un flux et les émettre. Pour commencer à écouter ce flux de données, il sera nécessaire de s'y subscribe. Pour arrêter l'écoute, on unsubscribe l'observable. C'est très utile lors de la gestion de variables qui sont mises à jour de manière asynchrone sur une même page, d'appels réguliers au back end, etc.
Dans l'exemple ci-dessous, la fonction interval() est un observable qui transmettra toutes les 200 secondes pour simuler un changement dans le flux :
Subject : c'est un sous-type du type Observable, bidirectionnel. Cela signifie qu'il nous permet de mettre à jour nous-mêmes la valeur de l'observable auquel il est associé, grâce à la méthode next().
Le principal avantage des observables est qu'ils ne se détruisent pas après la première mise à jour de la variable. Cette caractéristique peut entraîner un risque énorme si nous oublions d'arrêter l'écoute : cela créera une "fuite de mémoire".
Pour illustrer cela, nous allons créer un site web simple avec un bouton "Trigger". Lorsque nous cliquons dessus, une liste de 10000 nombres aléatoires sera créée chaque seconde. Sur notre page, nous verrons la somme de chaque liste. Après un second clic sur notre bouton, le composant sera détruit et les listes aussi.
Voici la fuite de mémoire : lorsque nous cliquons sur le bouton pour la première fois, nous créons un composant enfant qui gère les listes. Pour ce faire, un observable sera déclenché chaque seconde, et avec lui, la création d'une liste. Après le second clic sur le bouton, le composant enfant sera détruit, mais pas l'observable car nous n'avons pas clairement dit que nous voudrions nous unsubscribe. C'est pourquoi, lorsque nous faisons réapparaître le composant enfant, un nouvel observable est créé en plus de l'ancien. C'est pourquoi, plus nous déclenchons le bouton, plus nous voyons plusieurs valeurs apparaître simultanément sur notre écran.
Le premier indice d'une fuite de mémoire est... le temps de réponse. Plus nous effectuons d'actions sur notre site web, plus il faut de temps pour naviguer entre les pages. Dans ce cas, vous avez peut-être trouvé une fuite.
Si la fuite de mémoire est causée par des callbacks, vous pouvez le vérifier en voyant si vous avez des appels dupliqués dans la vue Network de votre navigateur. Deux autres vues qui peuvent vous aider à détecter les fuites dans d'autres cas sont :
L'utilisation d'unsubscribe est la méthode de base, la plus connue. Le principe est assez simple : nous stockons nos abonnements dans des objets de type Subscription. Et nous les détruisons quand nous voulons arrêter de les écouter. Le plus souvent, la destruction se fait dans la méthode ngOnDestroy (qui est la dernière étape du cycle de vie d'un composant Angular).
Il est possible d'utiliser la méthode .add() de la classe Subscription pour chaîner plusieurs abonnements. Ce faisant, si nous détruisons le premier observable de la chaîne, tous ceux qui suivent seront détruits. Cela évite d'écrire la ligne .unsubscribe() plusieurs fois. Bien sûr, cette méthode détruit tous les observables d'un coup, donc elle n'est pas à utiliser dans toutes les situations.
Une autre méthode pour gérer les abonnements de manière contrôlée est l'utilisation des fonctions RxJs dans le pipe() de l'observable. Il en existe plusieurs qui permettent de gérer chaque observable de manière unitaire : first(), take(), takeUntil() et takeWhile(). Essayons de comprendre leurs comportements respectifs :
First() : permet de récupérer la première valeur qui provient d'un observable qui respecte la condition indiquée (sinon, il ne prend que la première). C'est très utile quand on veut récupérer, par exemple, des données concernant une authentification. Nous allons créer un observable pour gérer l'asynchronisme le temps de vérifier un rôle et dès que l'information est retournée, nous détruirons l'observable.
ATTENTION : si vous mettez un prédicat et qu'il n'y a pas de correspondance, une exception est levée !
Take() : permet de récupérer les x premières valeurs d'un observable avant de le détruire. Il est souvent recommandé d'utiliser un take(1) plutôt qu'un first() si vous voulez la première valeur émise par l'observable. En effet, first() est préféré quand vous avez une logique (autre que l'ordre d'émission) dans la sélection, et il est plus susceptible de provoquer des erreurs.
TakeUntil() : c'est certainement l'un des outils les plus utiles pour gérer efficacement les observables. Il permet de mettre une condition de destruction sous forme de Subject. Dans les applications complexes où les observables ont le même cycle de vie, cette solution est vraiment à recommander par rapport à unsubscribe, en mettant le Subject à TRUE lors de la destruction du composant.
TakeWhile() : fonctionne comme un filtre sur les valeurs de l'observable. Il est rarement utilisé en pratique, sauf si vous voulez filtrer les retours d'un observable. Par exemple, nous lançons un appel sur une API Météo France pour récupérer les températures d'août, mais nous voulons récupérer les températures jusqu'à la première inférieure à 15°C.
Sans aucun doute, c'est le plus propre et le plus efficace (devant takeUntil) car il limite la quantité de code écrit. Il limite l'oubli car il associe le cycle de vie de l'observable à celui du HTML. Vous n'avez pas besoin de subscribe dans le '.ts' et vous appelez l'observable à l'endroit où vous voulez l'utiliser en ajoutant '| async'.
Même si cette méthode est la plus propre, elle a ses limites ! Comme expliqué, l'observable est résolu localement. Donc, si vous avez besoin d'utiliser la valeur dans des endroits largement espacés dans le HTML, pour éviter d'alourdir le HTML, vous devriez utiliser une des options ci-dessus. Cela évitera de dupliquer le code.
Les observables sont des outils puissants pour un site web dynamique. Mais les utiliser de manière non contrôlée pourrait grandement diminuer les performances de votre site web. Vous pouvez anticiper les problèmes de fuite de mémoire en utilisant différentes fonctions :
Sur le site web de Bpifrance, l'apprentissage des fuites de mémoire nous a permis de résoudre beaucoup de bugs et d'économiser jusqu'à 40% de temps de chargement sur certaines pages.