May 20, 2025

BroadcastChannel LeaderElection: Evitando Conexões WebSocket Duplicadas

Como usar BroadcastChannel com LeaderElection para eleger uma aba líder e evitar múltiplas conexões WebSocket, melhorando performance e UX em aplicações com microfrontends.

BroadcastChannel LeaderElection: Evitando Conexões WebSocket Duplicadas

Imagine o seguinte cenário:

Um usuário com duas abas abertas do painel de atendimento. Ambas tentam se conectar ao WebSocket para escutar eventos em tempo real.

🚨 O Problema

Quando múltiplas abas da mesma aplicação tentam se conectar simultaneamente ao WebSocket, você enfrenta:

  • Conexões duplicadas desnecessárias
  • Custo computacional elevado no backend
  • Tráfego de rede excessivo
  • UX inconsistente entre abas
  • Conflitos de estado em tempo real

✅ Solução: Eleger uma Aba Líder

Usamos a lib broadcast-channel que implementa LeaderElection de forma simples, confiável e com fallback pra browsers mais antigos.

Implementação Básica

import { BroadcastChannel, createLeaderElection } from 'broadcast-channel';

const channel = new BroadcastChannel('ws-coordinator');
const elector = createLeaderElection(channel);

elector.awaitLeadership().then(() => {
  console.log('Aba Principal. Conectando ao WebSocket...');
  const socket = new WebSocket('wss://api.example.com/ws');
  
  socket.onmessage = (event) => {
    // Processar mensagens do WebSocket
    const data = JSON.parse(event.data);
    console.log('Dados recebidos:', data);
  };
  
  socket.onclose = () => {
    console.log('WebSocket desconectado');
  };
});

🔄 Como Funciona

1. Eleição de Líder

  • Primeira aba que executa o código se torna líder
  • Apenas uma aba gerencia a conexão WebSocket
  • Eleição automática quando a aba líder é fechada

2. Comunicação Entre Abas

  • BroadcastChannel permite comunicação entre abas
  • Dados compartilhados em tempo real
  • Sincronização automática de estado

3. Fallback Robusto

  • Suporte a browsers antigos via IndexedDB
  • Detecção automática de aba fechada
  • Reeleição transparente quando necessário

🛠️ Implementação Avançada

Classe WebSocketManager

import { BroadcastChannel, createLeaderElection } from 'broadcast-channel';

class WebSocketManager {
  constructor(url, options = {}) {
    this.url = url;
    this.options = options;
    this.socket = null;
    this.channel = new BroadcastChannel('ws-coordinator');
    this.elector = createLeaderElection(this.channel);
    this.isLeader = false;
    
    this.setupLeaderElection();
    this.setupChannelListener();
  }
  
  setupLeaderElection() {
    this.elector.awaitLeadership().then(() => {
      this.isLeader = true;
      console.log('🎯 Aba eleita como líder');
      this.connectWebSocket();
    });
  }
  
  setupChannelListener() {
    this.channel.addEventListener('message', (event) => {
      if (!this.isLeader) {
        // Processar dados recebidos da aba líder
        this.handleWebSocketData(event.data);
      }
    });
  }
  
  connectWebSocket() {
    this.socket = new WebSocket(this.url);
    
    this.socket.onopen = () => {
      console.log('🔗 WebSocket conectado (aba líder)');
    };
    
    this.socket.onmessage = (event) => {
      const data = JSON.parse(event.data);
      
      // Broadcast para outras abas
      this.channel.postMessage({
        type: 'websocket-message',
        data: data
      });
      
      // Processar localmente também
      this.handleWebSocketData(data);
    };
    
    this.socket.onclose = () => {
      console.log('🔌 WebSocket desconectado');
      this.isLeader = false;
    };
    
    this.socket.onerror = (error) => {
      console.error('❌ Erro no WebSocket:', error);
    };
  }
  
  handleWebSocketData(data) {
    // Implementar lógica específica da aplicação
    console.log('📨 Dados processados:', data);
  }
  
  sendMessage(message) {
    if (this.isLeader && this.socket?.readyState === WebSocket.OPEN) {
      this.socket.send(JSON.stringify(message));
    } else {
      // Se não for líder, enviar via channel para o líder processar
      this.channel.postMessage({
        type: 'send-message',
        data: message
      });
    }
  }
  
  disconnect() {
    if (this.socket) {
      this.socket.close();
    }
    this.channel.close();
  }
}

Uso na Aplicação

// Inicializar o gerenciador
const wsManager = new WebSocketManager('wss://api.example.com/ws');

// Enviar mensagem (funciona de qualquer aba)
wsManager.sendMessage({
  type: 'user-action',
  payload: { action: 'click', element: 'button' }
});

// Cleanup ao sair da página
window.addEventListener('beforeunload', () => {
  wsManager.disconnect();
});

🎯 Vantagens da Solução

Performance

  • Evita conexões WebSocket duplicadas
  • Reduz custo computacional no backend
  • Diminui tráfego de rede desnecessário
  • Otimiza uso de recursos do browser

Experiência do Usuário

  • UX mais previsível e consistente
  • Sincronização automática entre abas
  • Comportamento uniforme independente da aba ativa
  • Transição transparente quando aba líder é fechada

Arquitetura

  • Ideal para Micro Frontends isolados
  • Comunicação eficiente entre componentes
  • Estado compartilhado sem complexidade
  • Escalabilidade para múltiplas abas

🔧 Casos de Uso Ideais

Painéis de Atendimento

  • Chat em tempo real sem duplicação
  • Notificações sincronizadas entre abas
  • Status de usuário consistente

Dashboards de Monitoramento

  • Métricas em tempo real compartilhadas
  • Alertas sincronizados entre abas
  • Performance otimizada do sistema

Aplicações Colaborativas

  • Edição simultânea sem conflitos
  • Presença de usuários sincronizada
  • Comunicação eficiente entre sessões

📋 Implementação com React

Hook Personalizado

import { useEffect, useRef, useState } from 'react';
import { BroadcastChannel, createLeaderElection } from 'broadcast-channel';

export function useWebSocketLeader(url) {
  const [isLeader, setIsLeader] = useState(false);
  const [data, setData] = useState(null);
  const wsManagerRef = useRef(null);
  
  useEffect(() => {
    const channel = new BroadcastChannel('ws-coordinator');
    const elector = createLeaderElection(channel);
    
    elector.awaitLeadership().then(() => {
      setIsLeader(true);
      console.log('🎯 Aba eleita como líder');
      
      const socket = new WebSocket(url);
      
      socket.onmessage = (event) => {
        const messageData = JSON.parse(event.data);
        
        // Broadcast para outras abas
        channel.postMessage({
          type: 'websocket-data',
          data: messageData
        });
        
        setData(messageData);
      };
      
      wsManagerRef.current = { socket, channel };
    });
    
    // Listener para dados de outras abas
    channel.addEventListener('message', (event) => {
      if (event.data.type === 'websocket-data') {
        setData(event.data.data);
      }
    });
    
    return () => {
      if (wsManagerRef.current) {
        wsManagerRef.current.socket.close();
        wsManagerRef.current.channel.close();
      }
    };
  }, [url]);
  
  return { isLeader, data };
}

Uso no Componente

import React from 'react';
import { useWebSocketLeader } from './useWebSocketLeader';

function Dashboard() {
  const { isLeader, data } = useWebSocketLeader('wss://api.example.com/ws');
  
  return (
    <div>
      <h1>Dashboard</h1>
      {isLeader && <span>🎯 Aba Líder</span>}
      {data && <div>Dados: {JSON.stringify(data)}</div>}
    </div>
  );
}

🚀 Benefícios em Produção

Para Desenvolvedores

  • Código mais limpo e organizado
  • Debugging facilitado com logs claros
  • Manutenção simplificada da comunicação
  • Testes mais confiáveis e previsíveis

Para Usuários

  • Performance melhorada da aplicação
  • Experiência consistente entre abas
  • Sincronização automática de dados
  • Comportamento previsível do sistema

Para Infraestrutura

  • Redução de carga no servidor WebSocket
  • Menor uso de banda de rede
  • Escalabilidade melhorada do sistema
  • Custos otimizados de infraestrutura

Este artigo apresenta uma solução prática para comunicação eficiente entre abas usando BroadcastChannel e LeaderElection. Para discussões técnicas, implementações ou dúvidas sobre WebSockets e microfrontends, me encontre no LinkedIn ou GitHub.