Comprendre le ramasse-miettes en JavaScript
Le ramasse-miettes gère automatiquement la mémoire en JavaScript en libérant les objets inaccessibles pour garantir une utilisation efficace des ressources.
Introduction au ramasse-miettes
Le ramasse-miettes gère automatiquement la mémoire en JavaScript. Il libère la mémoire utilisée par les données qui ne sont plus nécessaires, de sorte que vous n'avez presque jamais à allouer ou libérer de la mémoire manuellement. Cette page explique le concept fondamental sur lequel repose tout le système — l'accessibilité — puis aborde l'algorithme mark-and-sweep, les fuites qui subsistent malgré tout, et comment WeakMap/WeakSet vous aident à les éviter.
Comprendre ce mécanisme est important car « automatique » ne signifie pas « sans fuite ». Le collecteur ne supprime que ce qu'il peut prouver être inaccessible ; si votre code maintient une référence cachée en vie, la mémoire reste allouée pour toute la durée de vie du programme.
L'accessibilité : le concept fondamental
Le moteur ne suit pas si un object est « en cours d'utilisation » au sens logique. Il vérifie si l'objet est accessible — s'il existe une chaîne de références qui y mène depuis une racine.
Les racines sont des valeurs que le moteur conserve toujours :
- Les variables locales et les paramètres de la fonction en cours d'exécution.
- Les variables et fonctions de la chaîne actuelle d'appels imbriqués.
- Les variables globales (propriétés de
globalThis/window).
Tout objet accessible en suivant des références depuis une racine — directement ou via d'autres objets accessibles — est conservé. Tout le reste est considéré comme déchet.
Exemple : une référence maintient un objet en vie
Explication : L'objet { name: "John" } était accessible via user. Copier la référence dans admin crée un second chemin vers lui. Définir user = null supprime un chemin, mais admin pointe toujours vers l'objet, donc celui-ci reste accessible et n'est pas collecté.
Les objets interdépendants sont quand même collectés
Un mythe courant est que les objets qui se référencent mutuellement survivent. Ce n'est pas le cas — ce qui compte, c'est l'accessibilité depuis une racine, et non le fait que les objets se pointent les uns les autres.
Explication : obj1 et obj2 forment un cycle, mais une fois que les deux variables racines sont définies à null, il n'existe aucun chemin depuis une racine vers le cycle. L'ensemble de l'îlot devient inaccessible et éligible à la collecte. C'est pourquoi les moteurs JavaScript utilisent l'accessibilité plutôt qu'un simple comptage de références, qui provoquerait des fuites sur les cycles.
Comment fonctionne le collecteur : mark-and-sweep
Les moteurs JavaScript récupèrent la mémoire avec l'algorithme mark-and-sweep. Il réduit « cet objet n'est plus nécessaire » à la question précise « cet objet n'est plus accessible ».
- Marquage. À partir des racines, le collecteur visite chaque objet accessible et le marque. Il suit ensuite leurs références, marque ces objets, et ainsi de suite jusqu'à ce que tous les objets accessibles soient marqués.
- Balayage. Tout objet qui n'a pas été marqué est inaccessible. La mémoire qu'il occupe est libérée.
Vous ne pouvez pas déclencher cela manuellement et ne devriez pas essayer — il n'existe pas de gc() standard dans le langage. Les moteurs réels (comme V8) affinent l'algorithme de base avec des optimisations telles que la collecte générationnelle (les nouveaux objets meurent jeunes, donc on les vérifie plus souvent) et la collecte incrémentale (le travail est divisé en fragments pour éviter les pauses). Le modèle d'accessibilité que vous venez d'étudier reste identique.
Sources courantes de fuites mémoire
Une fuite en JavaScript est simplement un objet qui reste accessible alors que votre programme n'en a plus besoin. Le collecteur fonctionne correctement — il ne peut simplement pas détecter que la référence est périmée. Surveillez ces patterns.
Minuteries et intervalles oubliés
Un setInterval (ou setTimeout) en attente maintient son callback en vie, et le callback maintient tout ce qu'il capture par fermeture en vie. Si vous n'appelez jamais clearInterval, cette mémoire est conservée pour toute la durée de vie de la page.
Nœuds DOM détachés
Si vous supprimez un élément de la page mais conservez une référence à celui-ci dans une variable, le nœud — et l'ensemble de son sous-arbre — ne peut pas être collecté.
let detached = document.getElementById('list');
document.body.removeChild(detached);
// The node is gone from the page, but 'detached' still references it,
// so it stays in memory. Release it when done:
detached = null;Écouteurs d'événements persistants
Un écouteur lié à un élément DOM maintient à la fois l'élément et le gestionnaire (avec tout ce qu'il capture par fermeture) accessibles. Supprimez les écouteurs avec removeEventListener dès qu'ils ne sont plus nécessaires :
<button id="myButton">Click me</button>
<script>
const button = document.getElementById('myButton');
function alertClick() {
alert("Button clicked!");
button.removeEventListener('click', alertClick); // free the listener after first use
}
button.addEventListener('click', alertClick);
</script>Le bouton ne répond qu'au premier clic : le gestionnaire se supprime lui-même avec removeEventListener, libérant la référence afin qu'elle puisse être collectée par le ramasse-miettes.
Caches globaux qui ne font que croître
Un cache stocké dans un objet de niveau module ou global maintient chaque entrée accessible indéfiniment, à moins que vous ne supprimiez explicitement les anciennes. Une Map non bornée utilisée comme cache est une fuite lente classique.
Fermetures qui capturent plus que prévu
Une fermeture maintient en vie chaque variable des portées auxquelles elle fait référence — même celles qu'elle n'utilise jamais réellement. Retourner une petite fonction interne depuis une fonction avec de grandes variables locales peut épingler ces variables en mémoire. Gardez la portée capturée minimale, et consultez la portée des variables pour comprendre comment les fermetures conservent leur environnement.
Comment WeakMap et WeakSet aident
Map et Set détiennent des références fortes vers leurs clés/valeurs, donc tout ce qui y est stocké reste accessible. WeakMap et WeakSet détiennent leurs clés de manière faible : si la seule référence restante à un objet est celle à l'intérieur d'un WeakMap, l'objet peut quand même être collecté, et l'entrée disparaît avec lui.
Cela rend WeakMap idéal pour associer des données supplémentaires à des objets (caches, métadonnées, gestion des nœuds DOM) sans forcer ces objets à vivre indéfiniment. Comme les entrées peuvent disparaître à tout moment, WeakMap/WeakSet ne sont délibérément pas itérables et n'ont pas de propriété size.
Bonnes pratiques
- Préférez les variables locales ; elles sortent automatiquement de la portée et deviennent collectables lorsque la fonction se termine.
- Limitez les variables globales — elles vivent aussi longtemps que l'application. Utilisez des modules et la portée de bloc (
let/const). - Effacez les minuteries (
clearInterval/clearTimeout) et supprimez les écouteurs d'événements avecremoveEventListenerdès qu'ils ne sont plus nécessaires. - Annulez les références aux nœuds DOM détachés et aux autres grands objets dont vous avez fini l'utilisation en les mettant à null.
- Utilisez
WeakMap/WeakSetpour les caches et métadonnées indexés par object, afin que les entrées ne survivent pas à leurs clés.
Conclusion
Le ramasse-miettes en JavaScript repose entièrement sur l'accessibilité : le moteur conserve tout objet qu'il peut atteindre depuis une racine et libère le reste grâce au mark-and-sweep, qui gère même les cycles de références. « Automatique » vous laisse tout de même responsable de ne pas conserver de références périmées — les minuteries oubliées, les nœuds DOM détachés, les caches en croissance constante et les fermetures sur-capturantes sont les coupables habituels. Utilisez WeakMap et WeakSet lorsque vous souhaitez associer des données à des objets sans les maintenir en vie.