12k
All articles

Sincronização de Abas do Navegador com BroadcastChannel

BroadcastChannel sincroniza abas em tempo real com mensagens de structured clone, limpeza, fallback e padrões para auth, carrinho e tema.

OpenReplay Team
OpenReplay Team
Sincronização de Abas do Navegador com BroadcastChannel

A BroadcastChannel API é um barramento de mensagens nativo do navegador que permite que abas, janelas, iframes e workers da mesma origem se comuniquem em tempo real — a solução desenvolvida especificamente para o problema de dessincronização de estado entre abas, onde um logout em uma aba deixa as demais ainda exibindo uma interface autenticada. É uma API de quatro chamadas, sem servidor, sem handshake e sem dependências. Este artigo aborda a API mínima, o padrão de mensagens tipadas por ação que escala bem, a integração com frameworks com limpeza correta, os comportamentos não óbvios, e o suporte atual dos navegadores com um fallback funcional.

Principais Conclusões

  • A API completa consiste em quatro chamadas — new BroadcastChannel(name), postMessage(data), onmessage e close() — sem servidor, handshake ou configuração.
  • Como postMessage utiliza o algoritmo de clone estruturado, você envia objetos, Maps, Sets e Blobs diretamente — sem JSON.stringify, sem parsing manual no receptor.
  • Uma aba nunca recebe suas próprias transmissões: as mensagens são disparadas em todos os canais ouvintes exceto no objeto que as enviou, o que evita os loops de feedback que você precisaria tratar manualmente.
  • BroadcastChannel é um canal de transporte, não um repositório — ele transporta mensagens, mas não armazena nada; portanto, combine-o com localStorage ou IndexedDB para durabilidade e para hidratar abas abertas após o disparo de um evento.
  • BroadcastChannel é uma funcionalidade Baseline, amplamente disponível no Chrome, Firefox, Edge, Safari, Opera e Samsung Internet desde março de 2022, com suporte no Safari a partir da versão 15.4; apenas o Internet Explorer não o suporta.

O que é dessincronização de estado entre abas?

A dessincronização de estado entre abas é a classe de bug em que duas abas abertas do mesmo aplicativo mantêm estados divergentes: você faz logout em uma e a outra ainda exibe o painel, ou você esvazia um carrinho em uma aba e a outra ainda mostra os itens. Gravações de sessão desses fluxos frequentemente revelam o sintoma visível — um usuário enviando um pedido com base em um carrinho esvaziado em outra aba, ou continuando em uma aba que deveria ter sido desconectada — que é exatamente a dessincronização que o BroadcastChannel elimina.

Os desenvolvedores têm contornado esse problema com três soluções alternativas inferiores. O evento storage do localStorage dispara entre abas, mas transporta apenas strings, forçando serialização/parsing em cada mensagem e um gerenciamento de chaves pouco elegante. Fazer polling em um servidor ou no localStorage em intervalos é custoso e lento — você paga por verificações que, na maioria das vezes, não encontram nada. SharedWorker e WebSockets são ferramentas legítimas, mas uma viagem de ida e volta via servidor para sincronizar duas abas na mesma máquina é pesada demais para um problema puramente local.

MecanismoTipo de payloadPersiste?Requer redeMelhor uso
BroadcastChannelClone estruturado (objetos, Maps, Blobs)NãoNãoSincronização local entre abas/workers da mesma origem
Evento storageApenas stringsSim (localStorage)NãoSincronização simples com durabilidade nativa
SharedWorkerClone estruturadoNãoNãoComputação/conexão compartilhada entre abas
WebSocketStrings/binárioNo servidorSimDados enviados pelo servidor em tempo real para múltiplos clientes

A BroadcastChannel API em quatro chamadas

A API completa consiste em quatro chamadas, e qualquer contexto que construa um canal com o mesmo nome ingressa no mesmo barramento. Você conecta, escuta, envia e fecha:

const channel = new BroadcastChannel("app-sync");

channel.onmessage = (event) => {
  console.log("Received:", event.data);
};

channel.postMessage({ hello: "world" });

channel.close(); // on unmount / teardown

O payload é a principal vantagem. Os dados enviados via postMessage são serializados com o algoritmo de clone estruturado, portanto você passa objetos, arrays, Map, Set e Blob diretamente, sem necessidade de stringify — o receptor lê event.data como um objeto ativo. Symbols e SharedArrayBuffer são as exceções notáveis que não podem ser clonados. Os canais são restritos à mesma origem, e reutilizar uma única instância de canal por contexto é essencial: construir um novo BroadcastChannel a cada envio vaza listeners.

Como estruturar mensagens no BroadcastChannel?

O padrão que escala é uma mensagem discriminada — { type, payload } — com um único listener que faz switch em type e aplica cada ação ao estado local. Isso mantém todos os contextos seguindo o mesmo protocolo; a API em si não atribui significado às mensagens, então você define o seu.

const channel = new BroadcastChannel("cart-sync");
let cart = [];

channel.onmessage = ({ data }) => {
  if (data.type === "ADD_ITEM") cart.push(data.payload);
  else if (data.type === "CLEAR") cart = [];
  render();
};

function addItem(item) {
  cart.push(item);       // update this tab immediately
  render();
  channel.postMessage({ type: "ADD_ITEM", payload: item });
}

Observe que o remetente atualiza seu próprio estado diretamente antes de transmitir — como uma aba não recebe suas próprias mensagens, você aplica a mudança local de forma inline e deixa a transmissão se propagar para todos os demais.

Sincronizando autenticação, carrinho e configurações entre abas

Os casos de uso de maior valor são a sincronização de sessão (uma transmissão de logout limpa todas as abas), sincronização de carrinho, configurações e tema, e preenchimento automático de formulários em múltiplas abas. No React, crie o canal dentro de useEffect e retorne uma função de limpeza que chama close(); ignorar isso faz com que cada remontagem vaze um listener ativo.

import { useEffect } from "react";

function useAuthSync(onLogout) {
  useEffect(() => {
    const channel = new BroadcastChannel("auth");
    channel.onmessage = ({ data }) => {
      if (data.type === "LOGOUT") onLogout();
    };
    return () => channel.close();
  }, [onLogout]);
}

// on logout: clear local session, then
// new BroadcastChannel("auth").postMessage({ type: "LOGOUT" })

No Svelte 5 (runes), encapsule o canal em uma store. Um detalhe importante: postMessage precisa de um clone simples, não de um proxy reativo; portanto, passe o valor por $state.snapshot antes de transmitir — um proxy causaria falha na etapa de clone estruturado.

// theme.svelte.js — Svelte 5 (runes)
export class ThemeStore {
  value = $state("light");
  #channel = new BroadcastChannel("theme");
  constructor() {
    this.#channel.onmessage = ({ data }) => { this.value = data; };
  }
  set(next) {
    this.value = next;
    this.#channel.postMessage($state.snapshot(next));
  }
}

Os comportamentos que a maioria dos tutoriais ignora

Quatro comportamentos costumam comprometer implementações, e é justamente onde a maioria dos artigos se cala:

  • Uma aba não consegue ouvir suas próprias transmissões. As mensagens são disparadas em todos os canais ouvintes, exceto no objeto que enviou a mensagem — esse é o comportamento definido pela especificação, e é útil: ele previne os loops de feedback infinitos que você precisaria tratar com uma verificação de ID próprio. Aplique a mudança do remetente de forma inline.
  • “Mesma origem” significa, na prática, “mesma partição de armazenamento.” O armazenamento é particionado pelo site de nível superior, portanto um iframe cross-site não compartilhará um canal com a página de nível superior mesmo que estejam na mesma origem, e os canais nunca cruzam subdomínios diferentes.
  • É um canal de transporte, não um repositório. O BroadcastChannel transporta mensagens, mas não armazena nada. Combine-o com localStorage ou IndexedDB tanto para durabilidade quanto para hidratar abas abertas após o disparo de um evento — uma aba que abre tarde e nunca recebeu a transmissão deve ler o estado persistido na montagem.
  • Sempre chame close() na desmontagem. Fechar o canal desconecta o objeto e o libera para coleta de lixo; um canal criado por componente e nunca fechado vaza um listener a cada remontagem.

Suporte dos navegadores e um fallback com detecção de funcionalidade

O BroadcastChannel é Baseline e está amplamente disponível desde março de 2022, e o Safari atual o suporta completamente — a afirmação de que “não funciona no WebKit” é anterior ao Safari 15.4. De acordo com o caniuse e o blog Chrome for Developers, as versões mínimas são Chrome 54+, Edge 79+, Firefox 38+, Safari 15.4+ (macOS e iOS), Opera 41+ e Samsung Internet 7.2+; apenas o Internet Explorer nunca o implementou.

Para navegadores anteriores a 2022, detecte a funcionalidade com 'BroadcastChannel' in window e use um shim baseado no evento storage, expondo a mesma interface:

function createChannel(name) {
  if ("BroadcastChannel" in window) return new BroadcastChannel(name);
  // Fallback: localStorage "storage" event (strings only)
  return {
    postMessage: (data) =>
      localStorage.setItem(name, JSON.stringify({ data, t: Date.now() })),
    set onmessage(fn) {
      window.addEventListener("storage", (e) => {
        if (e.key === name && e.newValue) fn({ data: JSON.parse(e.newValue).data });
      });
    },
    close() {},
  };
}

Priorize a API nativa e trate o shim como uma medida de segurança. Em todos os navegadores atuais, o BroadcastChannel já está disponível — escolha um nome de canal, padronize em { type, payload }, persista o que precisa sobreviver a um fechamento completo, e o bug de aba desatualizada escondido no seu aplicativo desaparece.

Perguntas Frequentes

O BroadcastChannel funciona entre subdomínios diferentes?

Não. O BroadcastChannel é limitado à mesma partição de armazenamento, não apenas à mesma origem, e nunca cruza subdomínios diferentes. Uma página em app.example.com e uma página em account.example.com não podem compartilhar um canal. Mesmo na mesma origem, um iframe cross-site não compartilhará um canal com a página de nível superior, pois o armazenamento é particionado pelo site de nível superior. Para sincronização entre subdomínios, utilize um servidor ou WebSocket.

Qual é a diferença entre BroadcastChannel e o evento storage para sincronização entre abas?

O BroadcastChannel transporta payloads via clone estruturado — como objetos, Maps, Sets e Blobs — diretamente, mas não armazena nada. O evento storage do localStorage transporta apenas strings, obrigando você a serializar e fazer parsing de cada mensagem, mas grava estado durável como efeito colateral. Use o BroadcastChannel para sincronização rica orientada a mensagens e combine-o com localStorage ou IndexedDB quando também precisar de persistência ou de hidratar abas abertas após o disparo de um evento.

A aba que envia uma mensagem via BroadcastChannel recebe sua própria mensagem?

Não. Conforme a especificação, uma mensagem é disparada em todos os objetos BroadcastChannel que estão ouvindo o canal, exceto no objeto que a enviou. Uma aba nunca recebe suas próprias transmissões, o que previne os loops de feedback infinitos que você precisaria tratar com uma verificação de ID próprio. Por isso, aplique a mudança de estado do remetente de forma inline antes de chamar postMessage e deixe a transmissão se propagar para todos os outros contextos.

O que acontece com as mensagens do BroadcastChannel se todas as abas forem fechadas?

Elas são perdidas. O BroadcastChannel é um canal de transporte, não um repositório: ele transporta mensagens, mas não armazena nada; portanto, qualquer estado transmitido enquanto nenhuma outra aba estava ouvindo é perdido quando todas as instâncias são fechadas. Uma aba aberta após o disparo de um evento nunca receberá a mensagem original. Para sobreviver a isso, persista o estado no localStorage ou IndexedDB e hidrate as abas abertas posteriormente a partir desse armazenamento na montagem, em vez de depender apenas do canal.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.