Il y a quelques semaines, j’ai écrit un article sur les fuites de mémoire dans React lorsque utilisé en combinaison avec des hooks de mémorisation comme useMemo
ou useCallback
. L’article a attiré l’attention de plusieurs newsletters géniales telles que X, Y et Z. Naturellement, un certain nombre de personnes se demandaient comment la fuite que j’ai démontrée serait gérée par le nouveau compilateur React. Voici la réponse de l’équipe React : “le compilateur mettra en cache ces valeurs pour qu’elles ne soient pas constamment réallouées et que la mémoire ne croisse pas indéfiniment”. Cela semble prometteur, n’est-ce pas ? Mais, comme toujours, le diable se cache dans les détails. Plongeons dans le sujet et voyons comment le compilateur React gère le code de l’article précédent.
Dans l’article précédent, nous avons discuté de la façon dont les closures peuvent entraîner des fuites de mémoire dans React. L’essentiel : les closures capturent des variables de leur portée externe, dans les composants React, cela signifie souvent la capture de l’état, des props ou des valeurs mémorisées. Toutes les closures dans le composant partagent le même objet de style dictionnaire (objet de contexte) dans lequel les variables capturées sont stockées. L’objet de contexte est créé une fois lorsque la fonction du composant est appelée et reste en vie jusqu’à ce que la dernière closure puisse être collectée par le garbage collector. Chaque variable capturée sera ajoutée à cet objet. Si nous mettons en cache/mémorisons l’une des closures, elles maintiendront l’objet de contexte entier en vie, même si les autres closures ne sont plus utilisées. C’est particulièrement critique si l’une des closures fait référence à un grand objet, comme un grand tableau.
Vous avez peut-être entendu parler du nouveau compilateur React. L’équipe React travaille activement sur un nouvel outil de construction qui transformera votre code en une version plus optimisée. Il est encore en développement actif, mais l’objectif est de déléguer toute la mémorisation au compilateur. De cette façon, nous ne devrions pas avoir à nous soucier de l’endroit où placer nos useMemo
et useCallback
, et nous pourrons garder notre code concis et lisible. Si nous croyons la réponse de X concernant la fuite de closure, le compilateur mettra en cache “quelque chose”, ce qui empêchera alors la fuite. Mais, que met-il exactement en cache ? Et comment empêche-t-il la fuite ? Découvrons-le.
Voici le code de l’article précédent, où en cliquant alternativement sur les boutons “Incrémenter A” et “Incrémenter B”, une nouvelle allocation de A
se produira et ne sera jamais collectée par le garbage collector.
Si nous exécutons ce code à travers le compilateur React et essayons de reproduire la fuite de mémoire, nous verrons que l’utilisation de la mémoire reste constante, et nous n’avons qu’une seule instance de A
allouée dans l’instantané mémoire. Cela semble prometteur, n’est-ce pas ? Mais, examinons de plus près le code compilé pour voir ce que fait le compilateur.
J’ai ajouté le compilateur React au projet Vite et j’ai exécuté le code à travers lui. La sortie du compilateur est un peu verbeuse, mais la partie pertinente est la suivante : le compilateur a ajouté une nouvelle variable cache
qui est utilisée pour mettre en cache les valeurs mémorisées. Il a également en ligne les vérifications de référence qui invalident la valeur mise en cache dans le useMemo
. C’est ce qui s’est passé en coulisses avec le tableau de dépendances. Le A
est maintenant créé une seule fois et stocké dans le cache. Cela empêche la fuite de mémoire, car le A
n’est plus créé à chaque rendu du composant. Puisque A
n’a aucune dépendance sur un état ou des props, le compilateur suppose qu’il peut être mis en cache en toute sécurité. C’est équivalent à écrire const A = ...
dans le code original.
Le compilateur React est un excellent outil qui rendra probablement 98 % des bases de code plus performantes et plus faciles à lire. Pour les 2 % restants, cela pourrait ne pas suffire et même rendre plus difficile la compréhension du code. Une chose est sûre, le compilateur ne vous sauvera pas des fuites de mémoire causées par les closures. C’est, à mon avis, un problème fondamental avec JavaScript et la façon dont nous le combinons avec les paradigmes de programmation fonctionnelle. Les closures en JavaScript (du moins V8) ne sont tout simplement pas conçues pour une optimisation fine de la mémoire. C’est bien si vos composants se montent/démontent rapidement ou gèrent de plus petites quantités de données, mais cela peut poser un réel problème si vous avez des composants à longue durée de vie avec de grandes dépendances de données. Les idées à venir pourraient tourner autour du fait d’être explicite sur les dépendances des closures, comme dans l’exemple ci-dessus. Peut-être que nous verrons un wrapper intelligent qui rendra cela encore moins verbeux et plus ergonomique. Mais, pour l’instant, nous devons être conscients du problème et, comme toujours, garder à l’esprit les meilleures pratiques suivantes : écrire de petits composants, écrire des composants purs, écrire des fonctions/hooks personnalisés, utiliser le profileur de mémoire. J’espère que cet article vous a donné des informations sur le compilateur React et comment il gère les fuites de mémoire. Si vous avez des questions ou des commentaires, n’hésitez pas à me contacter sur X ou LinkedIn, ou laissez un commentaire ci-dessous. Merci de m’avoir lu ! 🚀