Découvrez les fuites de mémoire en JavaScript et ce que vous pouvez faire pour les réparer!
Dans cet article, nous allons explorer les typologies de fuite mémoire dans le code Javascript coté client. Nous allons aussi découvrir comment utiliser les outils de développement de Chrome pour les trouver. Commençons !
Introduction
Les fuites mémoires sont un problème auquel tous les développeurs sont confrontés, même lorsque vous travaillez avec des langages qui gèrent la mémoire, il y a des cas ou des fuites mémoires apparaissent. Les fuites sont la cause de toute une famille de problèmes : ralentissements, crashes, temps de réponse élevé, et même, des problèmes avec les autres applications.
Qu’est ce qu’une fuite mémoire ?
Une fuite mémoire peut essentiellement être définie comme une zone mémoire qui n’est plus utilisée par une application mais qui n’est pas libérée et donc pas rendue disponible à l’OS. Les langages ont différentes méthodes de gérer la mémoire. Ces méthodes réduisent les risques qu’une fuite mémoire se produise. Néanmoins, savoir si une zone mémoire est devenue inutile ou pas est un problème potentiellement insoluble. Seuls les concepteurs sont en mesure de clarifier si une zone mémoire est inutilisée. Wikipedia a de bons articles sur la gestion manuelle et automatique de la mémoire.
La gestion de la mémoire en JavaScript
JavaScript est un des langages avec Garbage collection. Les langages avec Garbage collection aident les développeurs a gérer la mémoire en vérifiant périodiquement quelle zone mémoire préalablement allouée doit rester accessible par d’autres parties de l’application. En d’autres termes, les langages à base de garbage collection réduisent la problématique de gestion de la mémoire de « quelle zone mémoire est encore requise » à « quelle zone mémoire reste accessible au reste de l’application ». La différence est subtile mais importante: tandis que seul le développeur sait quelle zone mémoire doit rester accessible dans le futur, les zones mémoires devenues inaccessibles peuvent être algorithmiquement identifiées et marquées pour être rendues à l’OS.
Les langages sans Garbage collection utilisent généralement d’autres techniques pour gérer la mémoire : gestion explicite de la mémoire, ou les développeurs indiquent au compilateur quand une zone mémoire n’est plus requise, et le comptage de référence, dans lequel un un compteur d’utilisation est associé avec chaque bloc de mémoire (quand il atteint zero, la zone est rendue à l’OS). Ces techniques viennent avec leurs propres contreparties (et potentiels risques de fuites).
Les fuites dans JavaScript
La raison principale des fuites dans les langages à Garbage collection sont les références non désirées. Pour comprendre ce que sont les références non désirées, nous devons d’abord comprendre comment le Garbage collector détermine si un zone de mémoire est accessible ou pas.
« La raison principale des fuites dans les langages à base de Garbage collector sont les références non désirées »
Marquer et balayer
La plupart des Garbage collectors utilisent un algorithme appelé mark-and-sweep (marquer et balayer). L’algorithme se compose des étapes suivantes :
- Le Garbage collector construit une liste de « racines ». Les racines sont généralement des variables globales qui sont référencées dans le code. Dans JavaScript, l’objet « windows » est un exemple de variable globale qui peut agir en tant que racine. L’objet Windows est toujours présent, donc le Garbage Collector peut la considérer – ainsi que tous ses enfants comme toujours présents (c’est à dire, pas du Garbage).
- Toutes les racines sont inspectées et marquées comme actives. Tous les enfants sont également inspectés de façon récursive. Tout ce qui peut être accessible depuis une racine n’est pas considéré comme du Garbage.
- Toutes les zones mémoires qui ne sont pas marquées comme actives peuvent alors être considérées comme du Garbage. Le Collector peut alors libérer ces zones mémoires et les rendre à l’OS.
La plupart des Garbage collector modernes sont construits sur cet algorithme avec des variantes mais le principe reste le même : toutes les zones mémoire accessibles sont marquées comme telles et le reste est considéré comme du Garbage.
Les références non désirées sont des références à des zones mémoires que le développeur sait ne plus utiliser mais qui – pour tout un tas de raisons – restent dans l’arbre d’une racine active. Dans le contexte de JavaScript, les références non désirées sont des variables qui sont gardées quelque part dans le code qui ne seront plus utilisées et pointent vers une zone mémoire qui aurait pu être libérée. Pour certains, c’est une erreur du développeur.
Afin de comprendre quelles sont les fuites les plus communes en JavaScript, il faut regarder de quelle façon les références sont souvent oubliées.
Les quatres types de fuite JavaScript :
1 : variables accidentellement globales
Un des objectifs de JavaScript était de développer un langage qui ressemble à Java mais qui soit suffisamment permissif pour être utilisé par des débutants. Cela se ressent dans la façon dont JavaScript gère les variables non déclarées : une référence à une variable non déclarée créée une nouvelle variable dans l’objet global. Dans le cad d’un navigateur, l’objet global est window. En d’autres termes :
function foo(arg) {
bar = "this is a hidden global variable";
}
Is in fact:
function foo(arg) {
window.bar = "this is an explicit global variable";
}
Si bar était censé contenir la référence à une variable uniquement à l’intérieur du périmètre de la fonction foo et que vous oubliez de la déclarer, une variable globale est créée par accident. Dans cet exemple, la fuite d’un String n’est pas très dangereuse mais cela pourrait être bien pire.
Une autre façon de créer accidentellement une variable globale est ça :
function foo() {
this.variable = "potential accidental global";
}
// Foo called on its own, this points to the global object (window)
// rather than being undefined.
foo();
Pour éviter à ces erreurs d’apparaitre, ajoutez ‘use strict’; au début de vos fichiers JavaScript. Cela déclenche un mode de parsing Javascript plus stricte qui empêche les globals par accident.
Note sur les variables globales
Bien que l’on parle de Globals insoupçonnées, la réalité est que beaucoup de codes sont jonchés de variables globales explicites. Elles sont par définition non collectables (à moins de les rendre nulles ou réaffectées). En particulier, les variables globales utilisées pour stocker et traiter de grandes quantités d’informations sont préoccupantes. Si vous devez utiliser des variables globales pour stocker beaucoup de données, assurez vous de les rendre nulles ou de les réaffecter une fois que vous avez terminé.
Une cause courante de l’augmentation de la consommation mémoire en relation avec les Globals est le cache. Le cache stocke des données qui sont utilisées à plusieurs reprises. Pour que ce soit efficace, le cache doit avoir une limite de taille plus importante. Les caches qui grandissent sans limite aboutissent à une grande consommation mémoire car le contenu ne peut pas être collecté.
2 : les timers oubliés ou les callbacks
L’utilisation de setInterval est assez commun en Javascript. Les autres librairies fournissent des observers et d’autres systèmes qui prennent en charge les callbacks. La plupart de ces librairies font en sorte de rendre les callback inaccessibles après que leurs instances elles-mêmes soient devenues inaccessibles. Dans le cas de setInterval, il est fréquent de voir du code comme ça :
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// Do stuff with node and someResource.
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
Cet exemple illustre ce qui peut arriver avec les timers ballottants : des timers qui font référence à un noeud ou des datas qui ne sont plus nécessaires. L’objet représenté par le noeud peut être supprimé dans le future, rendant inutile l’ensemble du bloc à l’intérieur du gestionnaire d’intervale.
Néanmoins, le gestionnaire, tout comme l’intervale est encore actif et ne peut donc pas être collecté (pour cela, il faudrait que que l’intervale soit stoppé). Si l’intervale ne peut pas être collecté, alors, ses dépendances non plus. Ce qui veut dire que les ressources, qui stockent des data potentiellement assez conséquentes, ne pourront pas non plus être collectées.
Dans le cas des observers, il est important de rendre explicites les call pour les supprimer quand ils ne sont plus requis (ou que les objets associés sont sur le point de devenir inaccessibles). Par le passé, c’était particulièrement important dans la mesure ou certains navigateurs (IE6) n’étaient pas capables de bien gérer les références cycliques (voir plus bas pour plus d’info la dessus)
Aujourd’hui, la plupart des browser pourront et feront la collecte des observer handlers une fois que l’objet observé sera devenu inaccessible. C’est une bonne pratique néanmoins de supprimer les observers avant que l’objet soit supprimé. Par exemple :
var element = document.getElementById('button');
function onClick(event) {
element.innerHtml = 'text';
}
element.addEventListener('click', onClick);
// Do stuff
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
// Now when element goes out of scope,
// both element and onClick will be collected even in old browsers that don't
// handle cycles well.
Note à propos des object observers et des références cycliques
Les observers et les références cycliques ont longtemps été le cauchemar des développeurs Javascript. C’était dû à un bug (ou une décision de conception) dans le Garbage collector d’Internet Explorer. Les anciennes versions d’IE ne pouvaient pas détecter les références cycliques entre les noeuds DOM et le code JavaScript. C’est caractéristique des observers, qui gardent généralement une référence sur l’observable (comme dans l’exemple ci dessus). En d’autres termes, à chaque fois qu’un observer est ajouté à un noeud dans Internet Explorer, ça conduisait à une fuite mémoire. C’est la raison pour laquelle les développeurs on commencé à supprimer manuellement les handlers avant les noeuds ou à nuller les références dans les observers. Aujourd’hui, les browsers modernes (incluant Internet Explorer et Microsoft Edge) utilisent des algorithmes de Garbage collection plus modernes qui peuvent détecter ces cycles et les traiter correctement. En d’autres termes, ce n’est plus nécéssaire d’appeler removeEventListener avant de rendre un noeud inaccessible.
Les frameworks comme jQuery suppriment les listeners avant de libérer les noeuds (quand ils utilisent leur API spécifiques pour ça). C’est géré en interne par la librairie et fait en sorte de ne générer aucune fuite, même sur les navigateurs plus anciens comme le vieil Internet Explorer.
3 : Les références en dehors du DOM
Parfois, ça peut-être utile de stocker des noeuds DOM dans les data structures. Supposez que vous voulez rapidement mettre à jour le contenu de plusieurs colonnes dans une table. Ca peut paraitre logique de stocker une référence à chaque colonne DOM dans un dictionnaire ou une array. Quand ça arrive, deux références au même élément DOM sont conservées : une dans l’arbre du DOM et l’autres dans le dictionnaire. Si a un moment donné, dans le futur, vous décidez de supprimer ces colonnes, vous devez rendre les deux références inaccessibles.
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text')
};
function doStuff() {
image.src = 'http://some.url/image';
button.click();
console.log(text.innerHTML);
// Much more logic
}
function removeButton() {
// The button is a direct child of body.
document.body.removeChild(document.getElementById('button'));
// At this point, we still have a reference to #button in the global
// elements dictionary. In other words, the button element is still in
// memory and cannot be collected by the GC.
}
Autre chose à prendre en considération : les références aux noeuds internes ou aux feuilles de l’arbre DOM. Supposez que par exemple, vous gardiez une référence vers une cellule spécifique du tableau (a <td> tag) dans votre code JavaScript. A un moment donné, vous décidez de supprimer le tableau spécifique du DOM mais de garder la référence à cette cellule. Intuitivement, on pourrait croire que le GC va collecter correctement tout sauf cette cellule. En pratique, ça n’arrivera pas: cette cellule est un noeud enfant du tableau et les enfants gardent des références vers leur parents. Ce qui signifie que l’ensemble du tableau va rester en mémoire, à cause de la référence JavaScript vers cette cellule. Prenez bien ça en compte si vous gardez des références vers des éléments DOM.
4: Cloture
Un élément clé du développement Javascript est la clôture : les fonctions anonymes qui capturent des variables du scope parent. Les développeurs Meteor ont rencontré un cas particulier, assez subtile de fuite mémoire dû aux détails de l’implémentation du runtime Javascript :
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing)
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log(someMessage);
}
};
};
setInterval(replaceThing, 1000);
Ce snippet fait une chose: à chaque fois que replaceThing est appelé, theThing récupère un nouvel objet qui contient une grande array et une nouvelle cloture (someMethod). En même temps, la variable inutilisée contient une clôture qui a une référence à originalThing (theThing de l’appel précédent vers replaceThing). Déjà un peu compliqué non ? L’important est qu’une fois que le scope est créé pour les clotures, qui sont dans le même scope parent, ce scope est partagé.
Dans ce cas, le scope créé pour la cloture someMethod est partagé avec unused. Unused a une référence vers originalThing. Bien que unused ne soit jamais utilisé., someMethod peut-être utilisé au travers de theThing. Et comme someMethod partage le scope de la cloture avec unused, même si unused n’est jamais utilisé, sa référence vers originalThing le force à rester actif (empêche sa collecte).
Quand ce snipet tourne à répétition, on observe une augmentation régulière de l’utilisation mémoire. Et cela ne réduit pas avec le passage du GC. En substance, une linked list de clôure est crée (avec pour racine la variable theThing) et chaque scope de ces closure contiennent une référence vers la grande array, ce qui génère une fuite conséquente.
C’est un artifact de l’implémentation. Une autre implémentation des clôtures qui gèrerait ce problème est concevable comme l’explique le blog Meteor.
Article original de Sebastián Peyrott traduit par JS Staff
Ne manquez la suite au prochain épisode : comment réparer les fuites mémoires 🙂
[separator type=”” size=”” icon=”star”]
[actionbox color=”default” title=”” description=”JS-REPUBLIC est une société de services spécialisée dans le développement JavaScript. Nous sommes centre de formation agréé. Retrouvez toutes nos formations techniques sur notre site partenaire dédié au Training” btn_label=”Nos formations” btn_link=”http://training.ux-republic.com” btn_color=”primary” btn_size=”big” btn_icon=”star” btn_external=”1″]