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.
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),onmessageeclose()— sem servidor, handshake ou configuração. - Como
postMessageutiliza o algoritmo de clone estruturado, você envia objetos, Maps, Sets e Blobs diretamente — semJSON.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.
| Mecanismo | Tipo de payload | Persiste? | Requer rede | Melhor uso |
|---|---|---|---|---|
| BroadcastChannel | Clone estruturado (objetos, Maps, Blobs) | Não | Não | Sincronização local entre abas/workers da mesma origem |
Evento storage | Apenas strings | Sim (localStorage) | Não | Sincronização simples com durabilidade nativa |
| SharedWorker | Clone estruturado | Não | Não | Computação/conexão compartilhada entre abas |
| WebSocket | Strings/binário | No servidor | Sim | Dados enviados pelo servidor em tempo real para múltiplos clientes |
A BroadcastChannel API em quatro chamadas
Discover how at OpenReplay.com.
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.