12k
All articles

Synchronisation des onglets de navigateur avec BroadcastChannel

BroadcastChannel synchronise les onglets en temps réel avec des messages en clone structuré, le nettoyage, un fallback et des modèles pour auth, panier et thème.

OpenReplay Team
OpenReplay Team
Synchronisation des onglets de navigateur avec BroadcastChannel

L’API BroadcastChannel est un bus de messages natif du navigateur qui permet aux onglets, fenêtres, iframes et workers d’une même origine de communiquer en temps réel — une solution conçue spécifiquement pour résoudre la dérive d’état multi-onglets, où une déconnexion dans un onglet laisse les autres afficher une interface utilisateur toujours connectée. C’est une API en quatre appels, sans serveur, sans handshake et sans dépendances. Cet article couvre l’API minimale, le pattern de messages typés par action qui passe à l’échelle, l’intégration dans les frameworks avec un nettoyage correct, les pièges non évidents, ainsi que la compatibilité navigateur actuelle avec un fallback fonctionnel.

Points clés à retenir

  • L’API entière se résume à quatre appels — new BroadcastChannel(name), postMessage(data), onmessage et close() — sans serveur, sans handshake ni configuration.
  • Comme postMessage utilise l’algorithme de clonage structuré, vous envoyez directement des objets, des Maps, des Sets et des Blobs — sans JSON.stringify, sans parsing manuel côté récepteur.
  • Un onglet ne reçoit jamais ses propres diffusions : les messages se déclenchent sur chaque canal à l’écoute sauf sur l’objet émetteur, ce qui évite les boucles de rétroaction infinies que vous auriez autrement dû gérer manuellement.
  • BroadcastChannel est un tuyau, pas un seau — il transporte les messages mais ne stocke rien ; associez-le à localStorage ou IndexedDB pour la persistance et pour hydrater les onglets ouverts après qu’un événement a été émis.
  • BroadcastChannel fait partie de la Baseline et est largement disponible sur Chrome, Firefox, Edge, Safari, Opera et Samsung Internet depuis mars 2022, avec l’arrivée du support Safari en version 15.4 ; seul Internet Explorer ne le prend pas en charge.

Qu’est-ce que la dérive d’état multi-onglets ?

La dérive d’état multi-onglets désigne la catégorie de bugs dans laquelle deux onglets ouverts d’une même application maintiennent des états divergents : vous vous déconnectez dans l’un et l’autre affiche toujours le tableau de bord, ou vous videz un panier dans l’un et l’autre affiche encore les articles. Les enregistrements de session de ces flux font régulièrement apparaître le symptôme visible — un utilisateur soumettant une commande sur un panier vidé ailleurs, ou continuant dans un onglet qui aurait dû être déconnecté — ce qui est précisément la désynchronisation qu’élimine BroadcastChannel.

Les développeurs ont contourné ce problème avec trois solutions de substitution moins efficaces. L’événement storage de localStorage se propage entre les onglets, mais ne transporte que des chaînes de caractères, imposant une sérialisation/désérialisation à chaque message et une gestion fastidieuse des clés. L’interrogation périodique d’un serveur ou de localStorage est coûteuse et introduit de la latence — vous payez pour des vérifications qui ne trouvent généralement rien. SharedWorker et WebSockets sont de vrais outils, mais un aller-retour réseau via un serveur pour synchroniser deux onglets sur la même machine est disproportionné pour un problème purement local.

MécanismeType de charge utilePersistance ?Réseau requisIdéal pour
BroadcastChannelClonage structuré (objets, Maps, Blobs)NonNonSynchronisation locale d’onglets/workers de même origine
Événement storageChaînes uniquementOui (localStorage)NonSynchronisation simple avec persistance intégrée
SharedWorkerClonage structuréNonNonCalcul/connexion partagés entre onglets
WebSocketChaînes/binaireCôté serveurOuiDonnées poussées par le serveur entre clients

L’API BroadcastChannel en quatre appels

L’API entière se résume à quatre appels, et tout contexte qui construit un canal avec le même nom rejoint le même bus. Vous vous connectez, écoutez, publiez et fermez :

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

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

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

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

La charge utile constitue l’avantage clé. Les données envoyées via postMessage sont sérialisées avec l’algorithme de clonage structuré, ce qui vous permet de passer directement des objets, des tableaux, des Map, des Set et des Blob sans les stringifier — le récepteur lit event.data comme un objet natif. Les Symbols et SharedArrayBuffer sont les exceptions notables qui ne peuvent pas être clonés. Les canaux sont limités à la même origine, et la réutilisation d’une seule instance de canal par contexte est importante : construire un nouveau BroadcastChannel à chaque envoi provoque des fuites de listeners.

Comment structurer les messages BroadcastChannel ?

Le pattern qui passe à l’échelle est un message discriminé — { type, payload } — associé à un unique listener qui bascule sur type et applique chaque action à l’état local. Cela garantit que chaque contexte parle le même protocole ; l’API elle-même n’attribue aucune signification aux messages, c’est donc à vous de la définir.

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

Notez que l’émetteur met à jour son propre état directement avant de diffuser — puisqu’un onglet n’entend pas ses propres messages, vous appliquez le changement local en ligne et laissez la diffusion se propager à tous les autres.

Synchronisation de l’authentification, du panier et des paramètres entre les onglets

Les cas d’usage les plus rentables sont la synchronisation de session (une diffusion de déconnexion efface tous les onglets), la synchronisation du panier, des paramètres et du thème, ainsi que le remplissage automatique de formulaires multi-onglets. Dans React, créez le canal à l’intérieur de useEffect et retournez un nettoyage qui appelle close() ; si vous l’omettez, chaque remontage du composant provoque une fuite de listener actif.

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" })

Dans Svelte 5 (runes), encapsulez le canal dans un store. Une mise en garde : postMessage nécessite un clone simple, et non un proxy réactif ; passez donc la valeur à travers $state.snapshot avant de diffuser — un proxy ferait échouer l’étape de clonage structuré.

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

Les pièges que la plupart des tutoriels ignorent

Quatre comportements font trébucher les implémentations, et ce sont précisément ceux sur lesquels la plupart des articles restent silencieux :

  • Un onglet ne peut pas entendre ses propres diffusions. Les messages se déclenchent sur chaque canal à l’écoute sauf sur l’objet émetteur — c’est un comportement défini par la spécification, et il est utile : il prévient les boucles de rétroaction infinies que vous auriez autrement dû contrer avec une vérification d’auto-identification. Appliquez le changement de l’émetteur en ligne.
  • « Même origine » signifie en réalité « même partition de stockage ». Le stockage est partitionné par site de premier niveau, donc une iframe cross-site ne partagera pas de canal avec une page de premier niveau même à la même origine, et les canaux ne franchissent jamais des sous-domaines différents.
  • C’est un tuyau, pas un seau. BroadcastChannel transporte les messages mais ne stocke rien. Associez-le à localStorage ou IndexedDB à la fois pour la persistance et pour hydrater les onglets qui s’ouvrent après qu’un événement a été émis — un onglet tardif qui n’a jamais reçu la diffusion lit l’état persisté au montage.
  • Appelez toujours close() au démontage. La fermeture déconnecte l’objet et le libère pour le ramasse-miettes ; un canal créé par composant et jamais fermé provoque une fuite de listener à chaque remontage.

Compatibilité navigateur et fallback avec détection de fonctionnalité

BroadcastChannel fait partie de la Baseline et est largement disponible depuis mars 2022 ; Safari le prend désormais entièrement en charge — l’affirmation selon laquelle « ça ne fonctionne pas dans WebKit » est antérieure à Safari 15.4. Selon caniuse et le blog Chrome for Developers, les versions minimales sont Chrome 54+, Edge 79+, Firefox 38+, Safari 15.4+ (macOS et iOS), Opera 41+ et Samsung Internet 7.2+ ; seul Internet Explorer ne l’a jamais implémenté.

Pour les navigateurs antérieurs à 2022, détectez la fonctionnalité avec 'BroadcastChannel' in window et repliez-vous sur un shim basé sur l’événement storage exposant la même 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() {},
  };
}

Privilégiez l’API native en premier et traitez le shim comme une assurance. Sur tous les navigateurs actuels, BroadcastChannel est déjà disponible — choisissez un nom de canal, standardisez sur { type, payload }, persistez ce qui doit survivre à une fermeture complète, et le bug d’onglet périmé qui se cache dans votre application disparaît.

FAQ

BroadcastChannel fonctionne-t-il entre différents sous-domaines ?

Non. BroadcastChannel est limité à la même partition de stockage, et non simplement à la même origine, et ne franchit jamais des sous-domaines différents. Une page sur app.example.com et une page sur account.example.com ne peuvent pas partager un canal. Même à la même origine, une iframe cross-site ne partagera pas de canal avec la page de premier niveau, car le stockage est partitionné par site de premier niveau. Pour la synchronisation entre sous-domaines, utilisez un serveur ou un WebSocket.

Quelle est la différence entre BroadcastChannel et l'événement storage pour la synchronisation inter-onglets ?

BroadcastChannel transporte directement des charges utiles utilisant le clonage structuré, telles que des objets, Maps, Sets et Blobs, mais ne stocke rien. L'événement storage de localStorage ne transporte que des chaînes de caractères, vous obligeant à sérialiser et désérialiser chaque message, mais il écrit un état durable en effet de bord. Utilisez BroadcastChannel pour une synchronisation riche orientée messages, et associez-le à localStorage ou IndexedDB lorsque vous avez également besoin de persistance ou d'hydrater des onglets ouverts après qu'un événement a été émis.

L'onglet qui envoie un message BroadcastChannel reçoit-il son propre message ?

Non. Conformément à la spécification, un message se déclenche sur chaque objet BroadcastChannel à l'écoute du canal, sauf sur l'objet émetteur. Un onglet n'entend jamais ses propres diffusions, ce qui prévient les boucles de rétroaction infinies que vous auriez autrement dû contrer avec une vérification d'auto-identification. Pour cette raison, appliquez le changement d'état de l'émetteur en ligne avant d'appeler postMessage, puis laissez la diffusion se propager à tous les autres contextes.

Que se passe-t-il avec les messages BroadcastChannel si tous les onglets sont fermés ?

Ils sont perdus. BroadcastChannel est un tuyau, pas un seau : il transporte les messages mais ne stocke rien, donc tout état diffusé alors qu'aucun autre onglet n'écoutait est perdu une fois toutes les instances fermées. Un onglet ouvert après qu'un événement a été émis ne reçoit jamais le message original. Pour survivre à cela, persistez l'état dans localStorage ou IndexedDB et hydratez les onglets qui s'ouvrent tardivement depuis ce stockage au montage, plutôt que de vous fier uniquement au 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.