← Retour au blog
websocketsrxjsreal-timeangularreactive-architecture

WebSockets et temps réel dans Angular : au-delà du polling, une architecture réactive solide

Les WebSockets offrent une communication bidirectionnelle persistante, mais intégrer cette capacité dans une application Angular complexe pose des défis architecturaux souvent sous-estimés. Trop de projets se contentent de brancher un WebSocket sur un composant, d'écouter les messages et d'appeler changeDetection.markForCheck(). Le résultat : une application fragile, difficile à tester, avec des connexions qui pendent mystérieusement et une synchronisation d'état chaotique. Cette approche naïve crée rapidement une dette technique massive. Une véritable architecture temps réel exige de penser en termes de flux de données, de résilience et de gestion explicite du cycle de vie de la connexion.

Architecturer la couche temps réel avec RxJS

La première décision est de ne jamais exposer le WebSocket directement aux composants. Créez un service dédié qui encapsule la logique de connexion et expose des observables RxJS. Ce service devient le point unique de contrôle pour tous les événements temps réel. Par exemple, un service NotificationService peut exposer un observable notifications$ qui émet automatiquement chaque message reçu. RxJS offre des opérateurs puissants pour transformer ces flux bruts en données applicatives stables. L'opérateur shareReplay() est critique : il cache le dernier message et permet à plusieurs souscripteurs de partager la même connexion WebSocket sans la dupliquer. Sans shareReplay(), chaque nouveau composant qui s'abonne créerait une nouvelle connexion WebSocket—une fuite de ressources classique.

export class RealtimeService {
  private socket$: Observable<WebSocket>;
  private subject = new Subject<Message>();

  notifications$ = this.subject.asObservable().pipe(
    shareReplay(1),
    catchError(err => {
      console.error('Notification stream error:', err);
      return throwError(() => err);
    })
  );

  constructor() {
    this.socket$ = this.createSocket().pipe(shareReplay(1));
  }

  private createSocket(): Observable<WebSocket> {
    return new Observable(observer => {
      const ws = new WebSocket('wss://api.example.com/ws');
      ws.onopen = () => observer.next(ws);
      ws.onerror = err => observer.error(err);
      return () => ws.close();
    });
  }
}```

Cette approche centralise la logique de connexion et permet à RxJS d'orchestrer les souscriptions. Les composants restent découplés de la mécanique WebSocket et peuvent se concentrer sur l'affichage des données. Le service gère transparemment la durée de vie de la connexion : quand le dernier souscripteur se désinscrit, la connexion se ferme automatiquement.

Gestion des reconnexions et résilience

Les connexions WebSocket échouent, c'est une certitude. Les réseaux mobiles basculant entre WiFi et 4G, les proxies qui tuent les connexions inactives après 30 secondes, les serveurs qui redémarrent. Une application de production doit gérer ces cas avec grâce. L'opérateur retryWhen() de RxJS permet de définir une stratégie de reconnexion exponentielle : attendre 1 seconde, puis 2, puis 4, avec un plafond raisonnable. Cette approche évite de surcharger le serveur en cas de panne massive. Combinez-la avec takeUntil() pour arrêter les tentatives quand le composant est détruit.

private createSocket(): Observable<WebSocket> {
  return new Observable<WebSocket>(observer => {
    const ws = new WebSocket('wss://api.example.com/ws');
    ws.onopen = () => observer.next(ws);
    ws.onmessage = (event) => this.subject.next(JSON.parse(event.data));
    ws.onerror = () => observer.error(new Error('WebSocket error'));
    ws.onclose = () => observer.error(new Error('WebSocket closed'));
    return () => ws.close();
  }).pipe(
    retryWhen(errors => errors.pipe(
      concatMap((error, index) => {
        const delayMs = Math.min(1000 * Math.pow(2, index), 30000);
        console.log(`Reconnecting in ${delayMs}ms...`);
        return timer(delayMs);
      })
    ))
  );
}```

La clé est de transformer les erreurs en opportunités de reconnexion, pas en défaites. Affichez un indicateur visuel : une petite icône ou une barre qui indique l'état de la connexion. Les utilisateurs doivent savoir que l'application tente de se reconnecter. Sans cet feedback, ils pensent que l'app est cassée et rafraîchissent la page—ce qui empire les choses.

Synchronisation d'état et gestion des conflits

Quand les données arrivent en temps réel et que l'utilisateur peut aussi modifier l'état localement, les conflits deviennent inévitables. Quelqu'un d'autre ajoute une tâche pendant que vous en modifiez une. L'approche naïve : remplacer purement et simplement l'état local par celui du serveur. Cela crée une expérience horrible : l'utilisateur tape du texte, qui disparaît soudainement. Une meilleure stratégie utilise une file d'attente optimiste. Vous appliquez les changements locaux immédiatement, mais vous envoyez aussi un message au serveur. Si le serveur rejette la modification, vous annulez le changement local. Si le serveur accepte, vous n'avez rien à faire—l'état est déjà correct.

Signal() de Angular 19+ facilite cette gestion. Stockez l'état dans un signal, exposez-le comme observable avec toSignal(). Quand un message arrive du serveur, fusionnez-le intelligemment avec l'état local. Un opérateur map() peut vérifier si le changement distant entre en conflit avec une opération locale en attente. Si oui, ignorez-le ou demandez au serveur de rejouer le changement après le vôtre. Cela crée une synchronisation quasi-transparente sans perte de données.

Pièges courants et anti-patterns

Le piège le plus courant est d'appeler markForCheck() ou detectChanges() manuellement après chaque message WebSocket. Cela signale que votre détection de changement Angular ne fonctionne pas correctement. Si vous devez marquer manuellement, vous avez un problème de zone ou d'asynchrone. Utilisez toujours RxJS avec async pipe ou signaux—Angular détectera automatiquement les changements. Deuxième piège : créer un service qui n'expose qu'une fonction subscribe() plutôt qu'un observable. Cela empêche l'utilisation d'opérateurs RxJS et crée du code collant dans les composants. Troisième piège : ne pas nettoyer les souscriptions. Chaque fois qu'un composant est détruit, les observables doivent se terminer proprement. Utilisez takeUntil(destroy$) systématiquement.

Quatrième piège : supposer que le WebSocket est toujours connecté. Testez ce qui se passe quand la connexion est fermée. Votre application affiche-t-elle un message clair ? Continue-t-elle à fonctionner en lecture seule ? Ou crash-t-elle ? La plupart des applications ne testent jamais ce scénario et échouent spectaculairement en production.

Conclusion : temps réel, pas magie

Implémenter le temps réel en Angular demande de la rigueur architecturale. Encapsulez les WebSockets dans un service RxJS, gérez les reconnexions avec des stratégies exponentielles, synchronisez l'état intelligemment et testez les scénarios de défaillance. Ne cherchez pas de solution universelle—chaque cas d'usage (notifications, chat, collaboration en temps réel, données financières) a ses propres exigences. Mais les principes restent : une couche temps réel testable, découplée des composants, et résiliente face aux défaillances réseau. Avec cette fondation, vous construirez des applications réactives sans cauchemars de maintenance.

Développeur Angular & Mobile freelance — Strasbourg.

© 2026 Emilien Pons — Tous droits réservés.Conçu avec Angular, PrimeNG et ❤️