12k
All articles

Sincronización de pestañas del navegador con BroadcastChannel

BroadcastChannel sincroniza pestañas en tiempo real con mensajes de clon estructurado, limpieza, fallback y patrones para auth, carrito y tema.

OpenReplay Team
OpenReplay Team
Sincronización de pestañas del navegador con BroadcastChannel

La API BroadcastChannel es un bus de mensajes nativo del navegador que permite que pestañas, ventanas, iframes y workers del mismo origen se comuniquen entre sí en tiempo real — la solución diseñada específicamente para la deriva de estado entre pestañas, donde cerrar sesión en una pestaña deja a las demás mostrando una interfaz como si el usuario siguiera autenticado. Es una API de cuatro llamadas sin servidor, sin handshake y sin dependencias. Este artículo cubre la API mínima, el patrón de mensajes tipados por acción que escala correctamente, la integración con frameworks con una limpieza adecuada, los comportamientos no evidentes a tener en cuenta, y el soporte actual en navegadores junto con un fallback ejecutable.

Puntos clave

  • La API completa se reduce a cuatro llamadas — new BroadcastChannel(name), postMessage(data), onmessage y close() — sin servidor, handshake ni configuración.
  • Dado que postMessage utiliza el algoritmo de clonación estructurada, puedes enviar objetos, Maps, Sets y Blobs directamente — sin JSON.stringify ni parseo manual en el receptor.
  • Una pestaña nunca recibe sus propias transmisiones: los mensajes se disparan en todos los canales que están escuchando excepto en el objeto que los envió, lo que evita los bucles de retroalimentación infinita contra los que de otro modo tendrías que protegerte manualmente.
  • BroadcastChannel es un conducto, no un almacén — transporta mensajes pero no guarda nada, así que combínalo con localStorage o IndexedDB para durabilidad y para hidratar pestañas abiertas después de que se haya disparado un evento.
  • BroadcastChannel es Baseline y está ampliamente disponible en Chrome, Firefox, Edge, Safari, Opera y Samsung Internet desde marzo de 2022, con soporte en Safari a partir de la versión 15.4; solo Internet Explorer carece de él.

¿Qué es la deriva de estado entre pestañas?

La deriva de estado entre pestañas es la categoría de error en la que dos pestañas abiertas de la misma aplicación mantienen estados divergentes: cierras sesión en una y la otra sigue mostrando el dashboard, o vacías el carrito en una y la otra sigue mostrando los artículos. Las reproducciones de sesión de estos flujos revelan con frecuencia el síntoma visible — un usuario enviando un pedido contra un carrito vaciado en otra pestaña, o continuando en una pestaña que debería haber cerrado sesión — que es exactamente la desincronización que BroadcastChannel elimina.

Los desarrolladores han parcheado este problema con tres soluciones alternativas inferiores. El evento storage de localStorage se dispara entre pestañas, pero solo transporta cadenas de texto, lo que obliga a serializar y parsear cada mensaje y a gestionar claves de forma engorrosa. Hacer polling al servidor o a localStorage en intervalos es costoso y lento — pagas por comprobaciones que en su mayoría no encuentran nada. SharedWorker y WebSockets son herramientas válidas, pero un viaje de ida y vuelta a través de un servidor para sincronizar dos pestañas en la misma máquina es demasiado pesado para un problema puramente local.

MecanismoTipo de payload¿Persiste?¿Requiere red?Mejor para
BroadcastChannelClonación estructurada (objetos, Maps, Blobs)NoNoSincronización local de pestañas/workers del mismo origen
Evento storageSolo cadenas de textoSí (localStorage)NoSincronización simple con durabilidad integrada
SharedWorkerClonación estructuradaNoNoCómputo/conexión compartida entre pestañas
WebSocketCadenas/binarioEn el servidorDatos enviados desde el servidor en tiempo real entre clientes

La API BroadcastChannel en cuatro llamadas

La API completa se reduce a cuatro llamadas, y cualquier contexto que construya un canal con el mismo nombre se une al mismo bus. Te conectas, escuchas, publicas y cierras:

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

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

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

channel.close(); // al desmontar / limpiar

El payload es la ventaja clave. Los datos enviados a través de postMessage se serializan con el algoritmo de clonación estructurada, por lo que puedes pasar objetos, arrays, Map, Set y Blob directamente sin necesidad de stringificar — el receptor lee event.data como un objeto activo. Los Symbols y SharedArrayBuffer son las excepciones notables que no se pueden clonar. Los canales son exclusivos del mismo origen, y reutilizar una instancia de canal por contexto es importante: construir un nuevo BroadcastChannel en cada envío genera fugas de listeners.

¿Cómo deberías estructurar los mensajes de BroadcastChannel?

El patrón que escala correctamente es un mensaje discriminado — { type, payload } — más un único listener que hace switch sobre type y aplica cada acción al estado local. Esto mantiene a todos los contextos hablando el mismo protocolo; la API en sí no asigna ningún significado a los mensajes, así que eres tú quien lo define.

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);       // actualizar esta pestaña inmediatamente
  render();
  channel.postMessage({ type: "ADD_ITEM", payload: item });
}

Observa que el emisor actualiza su propio estado directamente antes de transmitir — dado que una pestaña no escucha sus propios mensajes, aplicas el cambio local de forma inline y dejas que la transmisión se propague al resto.

Sincronización de autenticación, carrito y configuración entre pestañas

Los casos de uso de mayor valor son la sincronización de sesión (una transmisión de logout limpia todas las pestañas), la sincronización de carrito, configuración y tema, y el autocompletado de formularios entre pestañas. En React, crea el canal dentro de useEffect y devuelve una función de limpieza que llame a close(); si lo omites, cada remontaje genera una fuga de un listener activo.

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]);
}

// al cerrar sesión: limpiar la sesión local, luego
// new BroadcastChannel("auth").postMessage({ type: "LOGOUT" })

En Svelte 5 (runes), envuelve el canal en un store. Un aviso importante: postMessage necesita un clon plano, no un proxy reactivo, así que pasa el valor a través de $state.snapshot antes de transmitir — un proxy bloquearía el paso de clonación estructurada.

// 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));
  }
}

Los comportamientos que la mayoría de tutoriales omiten

Cuatro comportamientos suelen generar problemas en las implementaciones, y son precisamente donde la mayoría de los artículos guardan silencio:

  • Una pestaña no puede escuchar sus propias transmisiones. Los mensajes se disparan en todos los canales que escuchan excepto en el objeto que envió el mensaje — este es el comportamiento definido en la especificación, y resulta útil: evita los bucles de retroalimentación infinita contra los que de otro modo tendrías que protegerte con una comprobación de ID propio. Aplica el cambio del emisor de forma inline.
  • “Mismo origen” significa realmente “misma partición de almacenamiento.” El almacenamiento se particiona por sitio de nivel superior, por lo que un iframe cross-site no compartirá un canal con una página de nivel superior aunque tengan el mismo origen, y los canales nunca conectan diferentes subdominios.
  • Es un conducto, no un almacén. BroadcastChannel transporta mensajes pero no guarda nada. Combínalo con localStorage o IndexedDB tanto para durabilidad como para hidratar pestañas que se abran después de que se haya disparado un evento — una pestaña tardía que nunca recibió la transmisión lee el estado persistido al montarse.
  • Siempre llama a close() al desmontar. Cerrar el canal desconecta el objeto y lo libera para la recolección de basura; un canal creado por componente que nunca se cierra genera una fuga de listener en cada remontaje.

Soporte en navegadores y un fallback con detección de características

BroadcastChannel es Baseline y está ampliamente disponible desde marzo de 2022, y la versión actual de Safari lo soporta completamente — la afirmación de que “no funciona en WebKit” es anterior a Safari 15.4. Según caniuse y el blog de Chrome for Developers, las versiones mínimas son Chrome 54+, Edge 79+, Firefox 38+, Safari 15.4+ (macOS e iOS), Opera 41+ y Samsung Internet 7.2+; solo Internet Explorer nunca lo implementó.

Para navegadores anteriores a 2022, detecta la característica con 'BroadcastChannel' in window y recurre a un shim basado en el evento storage que expone la misma interfaz:

function createChannel(name) {
  if ("BroadcastChannel" in window) return new BroadcastChannel(name);
  // Fallback: evento "storage" de localStorage (solo cadenas de texto)
  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() {},
  };
}

Prioriza la API nativa y trata el shim como un seguro. En todos los navegadores actuales, BroadcastChannel ya está disponible — elige un nombre de canal, estandariza en { type, payload }, persiste lo que deba sobrevivir a un cierre completo, y el error de pestaña obsoleta que se esconde en tu aplicación desaparecerá.

Preguntas frecuentes

¿Funciona BroadcastChannel entre diferentes subdominios?

No. BroadcastChannel está limitado a la misma partición de almacenamiento, no solo al mismo origen, y nunca conecta diferentes subdominios. Una página en app.example.com y una página en account.example.com no pueden compartir un canal. Incluso en el mismo origen, un iframe cross-site no compartirá un canal con la página de nivel superior porque el almacenamiento se particiona por sitio de nivel superior. Para sincronización entre subdominios, utiliza un servidor o un WebSocket.

¿Cuál es la diferencia entre BroadcastChannel y el evento storage para la sincronización entre pestañas?

BroadcastChannel transporta payloads con clonación estructurada, como objetos, Maps, Sets y Blobs, de forma directa, pero no almacena nada. El evento storage de localStorage solo transporta cadenas de texto, lo que te obliga a serializar y parsear cada mensaje, aunque escribe estado duradero como efecto secundario. Usa BroadcastChannel para una sincronización rica orientada a mensajes y combínalo con localStorage o IndexedDB cuando también necesites persistencia o hidratar pestañas abiertas después de que se haya disparado un evento.

¿La pestaña que envía un mensaje de BroadcastChannel recibe su propio mensaje?

No. Según la especificación, un mensaje se dispara en todos los objetos BroadcastChannel que escuchan el canal excepto en el objeto que lo envió. Una pestaña nunca escucha sus propias transmisiones, lo que evita los bucles de retroalimentación infinita contra los que de otro modo tendrías que protegerte con una comprobación de ID propio. Por este motivo, aplica el cambio de estado del emisor de forma inline antes de llamar a postMessage, y deja que la transmisión se propague a todos los demás contextos.

¿Qué ocurre con los mensajes de BroadcastChannel si se cierran todas las pestañas?

Se pierden. BroadcastChannel es un conducto, no un almacén: transporta mensajes pero no guarda nada, por lo que cualquier estado transmitido mientras ninguna otra pestaña estaba escuchando se pierde una vez que todas las instancias se cierran. Una pestaña abierta después de que se disparó un evento nunca recibe el mensaje original. Para sobrevivir a esto, persiste el estado en localStorage o IndexedDB e hidrata las pestañas que se abran tarde desde ese almacenamiento al montarse, en lugar de depender únicamente del 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.