Getting started General
Components
Forms
Trends
Utilities
Migrate from v1
  Join us
  WebSockets Articles

Pourquoi les objets WebSocket ne sont pas detruits quand ils sortent du scope en JavaScript ?

WebSockets

Nous avons recemment eu une erreur en utilisant des WebSockets. Les objets WebSocket restaient bloques en memoire meme apres etre sortis du scope. La reponse a ce probleme se trouve dans la maniere dont JavaScript gere les event listeners et son garbage collection. Nous allons voir pourquoi cela arrive et comment nettoyer correctement vos connexions WebSocket.

Pourquoi les objets WebSocket ne sont pas detruits quand ils sortent du scope
Comprendre la gestion memoire des WebSocket et le garbage collection

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.

Commencez à créer avec Axentix

Prêt à créer des sites web exceptionnels ? Commencez avec le framework Axentix dès aujourd'hui.

Commencer

Articles similaires