Si vous avez deja travaille avec des WebSockets en JavaScript, vous avez peut-etre remarque quelque chose d'etonnant : meme apres qu'un objet WebSocket sorte du scope, il n'est pas collecte par le garbage collector. Vous pouvez creer un WebSocket dans une fonction, laisser cette fonction se terminer, et pourtant la connexion WebSocket reste active en memoire.
Ce comportement peut etre deroutant au debut, mais il est volontaire. La raison est liee aux event listeners et a la facon dont le runtime JavaScript gere les operations asynchrones.
Le probleme : des WebSockets qui ne meurent pas
Regardons un scenario courant qui peut vous laisser perplexe :
function connectToServer() {
const ws = new WebSocket('wss://example.com/ws');
ws.onopen = () => {
console.log('Connected!');
};
ws.onmessage = (event) => {
console.log('Message:', event.data);
};
ws.onerror = (error) => {
console.error('Error:', error);
};
// Function ends, ws goes out of scope...
// But the WebSocket is still alive!
}
connectToServer();
// Even though the function finished, the WebSocket connection persists
Vous pourriez penser qu'une fois que connectToServer() a fini
de s'executer, la variable ws serait eligible au garbage
collection. Mais ce n'est pas ce qui se passe. Le WebSocket reste en vie, continue d'ecouter les messages et garde
la connexion ouverte.
Pourquoi cela arrive : les event listeners gardent les objets en vie
La cle pour comprendre ce comportement est de reconnaitre que les event listeners empechent le garbage collection. Voici pourquoi :
Quand vous attachez des event listeners a un WebSocket (comme onopen, onmessage, etc.), le runtime
JavaScript doit garder une trace de ces listeners. Tant que la connexion WebSocket est ouverte, le moteur du
navigateur doit pouvoir appeler ces handlers quand des evenements arrivent.
Pour cela, le runtime maintient une reference vers l'instance WebSocket. Cette reference agit comme une "root" pour le garbage collector : cela signifie que l'objet WebSocket ne peut pas etre collecte tant que le runtime doit encore appeler ces event listeners.
function connectToServer() {
const ws = new WebSocket('wss://example.com/ws');
// These event listeners create a reference that the runtime must maintain
ws.onmessage = (event) => {
// The runtime needs to keep 'ws' alive to call this function
console.log('Message:', event.data);
};
// Even though 'ws' goes out of scope here,
// the runtime still holds a reference to it
// because it needs to call onmessage when data arrives
}
Pensez a cela ainsi : le runtime JavaScript maintient une liste d'objets "actifs" qui servent de racines (roots) pour le garbage collection. Ce sont des objets dont le runtime sait qu'il aura besoin dans le futur. Tant que votre WebSocket a une connexion ouverte et des event listeners actifs, il reste dans cette liste.
Quand les WebSockets peuvent-ils etre garbage collectes ?
Un WebSocket devient eligible au garbage collection seulement quand :
- Tous les event listeners sont retires - plus aucun callback n'a besoin d'etre appele
- La connexion est fermee - plus aucun evenement ne peut arriver
- Aucune autre reference n'existe - rien d'autre dans votre code ne retient le WebSocket
Une fois ces conditions reunies, le runtime peut retirer le WebSocket de sa liste active et laisser le garbage collector le nettoyer.
function connectToServer() {
const ws = new WebSocket('wss://example.com/ws');
ws.onmessage = (event) => {
console.log('Message:', event.data);
};
// Close the connection and remove listeners
setTimeout(() => {
ws.close(); // Close the connection
ws.onmessage = null; // Remove the listener
ws.onopen = null;
ws.onerror = null;
ws.onclose = null;
// Now the WebSocket can be garbage collected
}, 5000);
}
Cela s'applique a toutes les Web APIs asynchrones
Ce comportement n'est pas unique aux WebSockets. Le meme principe s'applique a toutes les Web APIs asynchrones en JavaScript :
- Network APIs : WebSocket, XMLHttpRequest, fetch, RTCPeerConnection
- Timers : setTimeout, setInterval, requestAnimationFrame
- File System : FileReader
- DOM : l'objet document entier et tous ses event listeners
Tout cela cree des objets que le runtime doit garder en vie tant qu'ils ont des listeners actifs ou des operations en attente. C'est ce qui maintient votre application en fonctionnement apres que le code d'initialisation a fini de s'executer.
// setTimeout example
function startTimer() {
const timerId = setTimeout(() => {
console.log('Timer fired!');
}, 1000);
// timerId goes out of scope, but the timer keeps running
// The runtime holds a reference to keep the callback alive
}
// XMLHttpRequest example
function fetchData() {
const xhr = new XMLHttpRequest();
xhr.onload = () => {
console.log('Data loaded');
};
xhr.open('GET', '/api/data');
xhr.send();
// xhr goes out of scope, but the request continues
// The runtime keeps it alive to call onload when ready
}
Bonnes pratiques : nettoyer correctement les WebSockets
Pour eviter les fuites memoire et assurer un nettoyage correct, fermez toujours explicitement les connexions WebSocket quand vous n'en avez plus besoin :
class WebSocketManager {
constructor() {
this.ws = null;
}
connect(url) {
// Close existing connection if any
this.disconnect();
this.ws = new WebSocket(url);
this.ws.onopen = () => {
console.log('Connected');
};
this.ws.onmessage = (event) => {
this.handleMessage(event.data);
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
this.ws.onclose = () => {
console.log('Connection closed');
// Clean up references
this.ws = null;
};
}
disconnect() {
if (this.ws) {
// Remove all event listeners
this.ws.onopen = null;
this.ws.onmessage = null;
this.ws.onerror = null;
this.ws.onclose = null;
// Close the connection
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.close();
}
this.ws = null;
}
}
handleMessage(data) {
// Process the message
console.log('Received:', data);
}
}
// Usage
const manager = new WebSocketManager();
manager.connect('wss://example.com/ws');
// Later, when done
manager.disconnect(); // Properly cleans up
Utiliser addEventListener au lieu d'une assignation directe
Si vous utilisez addEventListener,
assurez-vous de retirer les listeners avec removeEventListener :
function connectWithListeners() {
const ws = new WebSocket('wss://example.com/ws');
const messageHandler = (event) => {
console.log('Message:', event.data);
};
ws.addEventListener('message', messageHandler);
// To properly clean up:
ws.removeEventListener('message', messageHandler);
ws.close();
}
Comprendre les racines (roots) du garbage collection
Le garbage collector de JavaScript utilise un concept appele "roots" pour determiner quels objets sont encore necessaires. Ces roots sont des objets que le runtime sait qu'il doit garder en vie. Des roots courantes incluent :
- Variables globales - variables dans le scope global
- Scopes de fonctions actifs - variables dans des fonctions en cours d'execution
- Event listeners - objets avec des handlers enregistres
- Timers actifs - objets references par setTimeout/setInterval
- Connexions ouvertes - connexions reseau encore actives
Tant qu'un objet est atteignable depuis une root, il ne peut pas etre garbage collecte. Votre WebSocket devient une root lui-meme quand il a des event listeners actifs, ce qui explique pourquoi il persiste meme apres etre sorti du scope.
Pieges courants et comment les eviter
Piege 1 : oublier de fermer les connexions
// ❌ Bad: Connection never closes
function badExample() {
const ws = new WebSocket('wss://example.com/ws');
ws.onmessage = () => console.log('Got message');
// Connection stays open forever
}
// ✅ Good: Explicit cleanup
function goodExample() {
const ws = new WebSocket('wss://example.com/ws');
ws.onmessage = () => console.log('Got message');
// Clean up after 30 seconds
setTimeout(() => {
ws.close();
ws.onmessage = null;
}, 30000);
}
Piege 2 : creer plusieurs connexions
// ❌ Bad: Creates new connection on every call
function connect() {
const ws = new WebSocket('wss://example.com/ws');
ws.onmessage = handleMessage;
}
// User clicks button multiple times = multiple connections!
// ✅ Good: Reuse or close existing connection
let ws = null;
function connect() {
if (ws && ws.readyState === WebSocket.OPEN) {
return; // Already connected
}
if (ws) {
ws.close(); // Close old connection
}
ws = new WebSocket('wss://example.com/ws');
ws.onmessage = handleMessage;
}
Piege 3 : ne pas nettoyer dans le lifecycle des composants
// React example
function ChatComponent() {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket('wss://example.com/chat');
ws.onmessage = (event) => {
setMessages(prev => [...prev, event.data]);
};
// ✅ Cleanup function
return () => {
ws.close();
ws.onmessage = null;
};
}, []);
return {/* render messages */};
}
Resume : points cles
- Les event listeners gardent les objets en vie : tant qu'un WebSocket a des event listeners actifs, le runtime le garde en memoire pour appeler ces listeners quand des evenements arrivent.
- C'est par design : le runtime doit maintenir des references vers les objets avec des operations asynchrones en attente.
- Fermer explicitement les connexions : appelez toujours
close()et retirez les event listeners quand vous avez fini avec un WebSocket. - Cela s'applique largement : le meme comportement s'applique a toutes les Web APIs asynchrones, pas seulement aux WebSockets.
- Racines du garbage collection : les objets avec des listeners actifs deviennent des roots qui empechent le garbage collection.
Conclusion
Les objets WebSocket ne sont pas detruits quand ils sortent du scope parce que le runtime JavaScript doit les garder en vie pour appeler leurs event listeners. C'est le meme mecanisme qui maintient votre application en execution apres que le code initial a fini de s'executer.
Comprendre ce comportement vous aide a ecrire un meilleur code et a eviter les fuites memoire. Retenez de fermer explicitement les connexions WebSocket et de retirer les event listeners quand vous avez fini. Cela assure un nettoyage correct et evite une utilisation memoire inutile.
La conclusion cle est simple : si quelque chose peut arriver de facon asynchrone, le runtime doit garder l'objet en vie pour le gerer. C'est vrai pour les WebSockets, les timers, les requetes reseau et toutes les autres operations asynchrones en JavaScript.