Pourquoi synchroniser plusieurs onglets ?

Une application web moderne n’est pas toujours ouverte dans un seul onglet. Un utilisateur peut avoir un tableau de bord dans une fenêtre, une page de profil dans une autre, puis revenir sur une ancienne recherche quelques minutes plus tard. Si l’état de ces onglets diverge, l’expérience devient vite incohérente : session déconnectée dans un onglet mais encore active dans un autre, thème sombre appliqué seulement sur une page, panier mis à jour sans être reflété ailleurs, cache local obsolète après une mutation.

On peut résoudre une partie de ces problèmes avec le stockage local, les événements storage, un cache applicatif ou des requêtes régulières vers le serveur. Mais ces solutions sont parfois indirectes, bruyantes ou trop coûteuses. La BroadcastChannel API propose une approche plus simple : ouvrir un canal nommé et diffuser des messages à tous les autres contextes du même site.

Elle est particulièrement utile en complément d’un cache frontend stale-while-revalidate, d’une application utilisant des Service Workers, ou d’interfaces riches dans lesquelles l’état doit rester cohérent entre plusieurs vues.

Le principe de BroadcastChannel

BroadcastChannel permet à plusieurs documents, onglets, fenêtres, iframes ou workers d’une même origine de communiquer par messages. Chaque contexte crée un canal avec le même nom. Lorsqu’un contexte envoie un message, les autres contextes abonnés au canal le reçoivent.

Voici un premier exemple minimal :

const channel = new BroadcastChannel('app');

channel.postMessage({
  type: 'theme:changed',
  theme: 'dark'
});

channel.addEventListener('message', (event) => {
  console.log('Message reçu depuis un autre contexte', event.data);
});

Le message n’est pas envoyé au même objet BroadcastChannel qui l’a émis, mais aux autres instances connectées au même canal. C’est précisément ce que l’on veut dans la plupart des cas : prévenir les autres onglets qu’un événement vient de se produire.

La donnée transmise utilise l’algorithme de clonage structuré. Vous pouvez donc envoyer des objets, tableaux, nombres, chaînes, booléens ou données plus complexes compatibles avec ce mécanisme. En revanche, il faut éviter de transmettre des fonctions, des instances non sérialisables ou des objets inutilement volumineux.

Typage des messages en TypeScript

Dans une application réelle, envoyer des objets anonymes devient vite fragile. Il vaut mieux définir un protocole de messages explicite avec une union discriminée TypeScript.

type AppMessage =
  | { type: 'auth:logout' }
  | { type: 'theme:changed'; theme: 'light' | 'dark' }
  | { type: 'cart:updated'; cartId: string; updatedAt: string }
  | { type: 'cache:invalidate'; key: string };

const channel = new BroadcastChannel('formation-dev-web');

function publish(message: AppMessage) {
  channel.postMessage(message);
}

channel.addEventListener('message', (event: MessageEvent<AppMessage>) => {
  const message = event.data;

  switch (message.type) {
    case 'auth:logout':
      redirectToLogin();
      break;

    case 'theme:changed':
      document.documentElement.dataset.theme = message.theme;
      break;

    case 'cart:updated':
      refreshCart(message.cartId);
      break;

    case 'cache:invalidate':
      invalidateCacheEntry(message.key);
      break;
  }
});

Cette structure rend le code plus lisible et réduit les erreurs. Elle rappelle la logique des machines à états en TypeScript : au lieu de disperser des booléens et des chaînes arbitraires dans l’application, on formalise les événements possibles.

Attention toutefois : TypeScript ne valide pas les données à l’exécution. Si votre canal peut recevoir des messages issus de zones moins contrôlées de votre code, vous pouvez ajouter une validation runtime, comme pour le typage des données API en TypeScript.

Cas pratique : synchroniser une déconnexion

La déconnexion est l’un des meilleurs cas d’usage. Si l’utilisateur clique sur “Se déconnecter” dans un onglet, les autres onglets ne doivent pas continuer à afficher une interface authentifiée.

type AuthMessage = { type: 'auth:logout' };

const authChannel = new BroadcastChannel('auth');

export function logout() {
  localStorage.removeItem('access_token');

  authChannel.postMessage({
    type: 'auth:logout'
  } satisfies AuthMessage);

  window.location.assign('/connexion');
}

authChannel.addEventListener('message', (event: MessageEvent<AuthMessage>) => {
  if (event.data.type === 'auth:logout') {
    localStorage.removeItem('access_token');
    window.location.assign('/connexion');
  }
});

Ce mécanisme ne remplace pas la sécurité côté serveur. Une session doit toujours être invalidée côté backend lorsque c’est nécessaire. BroadcastChannel améliore seulement la cohérence de l’interface entre onglets.

Cas pratique : invalider un cache local

Imaginons une application avec un cache applicatif en mémoire ou dans IndexedDB. Quand un onglet modifie une ressource, les autres onglets doivent éviter d’afficher une ancienne version.

type CacheMessage = {
  type: 'cache:invalidate';
  resource: 'profile' | 'orders' | 'cart';
  id?: string;
};

const cacheChannel = new BroadcastChannel('cache');

export function notifyCacheInvalidation(message: Omit<CacheMessage, 'type'>) {
  cacheChannel.postMessage({
    type: 'cache:invalidate',
    ...message
  });
}

cacheChannel.addEventListener('message', (event: MessageEvent<CacheMessage>) => {
  const message = event.data;

  if (message.type === 'cache:invalidate') {
    removeFromLocalCache(message.resource, message.id);
  }
});

Après une mutation, vous pouvez publier une invalidation :

await updateProfile(profile);

notifyCacheInvalidation({
  resource: 'profile',
  id: profile.id
});

Cette approche fonctionne bien avec une stratégie stale-while-revalidate : les onglets peuvent supprimer ou marquer comme périmées certaines entrées, puis relancer une récupération de données au prochain affichage.

Intégration dans React

Dans React, il faut éviter de créer un nouveau canal à chaque rendu. Une solution simple consiste à encapsuler la logique dans un module ou dans un hook avec nettoyage explicite.

import { useEffect } from 'react';

type ThemeMessage = {
  type: 'theme:changed';
  theme: 'light' | 'dark';
};

export function useThemeBroadcast(onThemeChange: (theme: 'light' | 'dark') => void) {
  useEffect(() => {
    const channel = new BroadcastChannel('theme');

    function handleMessage(event: MessageEvent<ThemeMessage>) {
      if (event.data.type === 'theme:changed') {
        onThemeChange(event.data.theme);
      }
    }

    channel.addEventListener('message', handleMessage);

    return () => {
      channel.removeEventListener('message', handleMessage);
      channel.close();
    };
  }, [onThemeChange]);
}

Puis, lorsqu’un utilisateur change de thème :

const channel = new BroadcastChannel('theme');

function setTheme(theme: 'light' | 'dark') {
  document.documentElement.dataset.theme = theme;
  localStorage.setItem('theme', theme);

  channel.postMessage({
    type: 'theme:changed',
    theme
  } satisfies ThemeMessage);
}

Dans une base de code plus structurée, on évitera de créer ce canal directement dans le composant. Un service dédié ou un gestionnaire d’événements applicatif rendra l’ensemble plus testable.

BroadcastChannel, storage event ou WebSocket ?

BroadcastChannel n’est pas un WebSocket. Il ne communique pas avec un serveur et ne permet pas d’échanger des messages entre utilisateurs. Pour diffuser des informations temps réel depuis un backend, il faut plutôt regarder du côté des Server-Sent Events ou des WebSockets.

Il ne remplace pas non plus complètement l’événement storage. Celui-ci reste pratique lorsqu’une modification de localStorage doit être détectée dans les autres onglets. Mais BroadcastChannel a plusieurs avantages : il ne nécessite pas d’écrire dans le stockage, il accepte des messages plus expressifs et il décrit mieux l’intention du code.

Quant aux workers, ils peuvent aussi participer à certains scénarios de communication. Si votre objectif est surtout de déplacer des calculs coûteux hors du thread principal, les Web Workers en JavaScript restent plus adaptés. BroadcastChannel répond à un autre problème : propager un événement entre contextes.

Bonnes pratiques

Commencez par nommer vos canaux avec précision. Un canal app peut suffire pour un petit projet, mais des noms comme auth, cache, theme ou notifications deviennent plus lisibles quand l’application grandit.

Définissez ensuite un protocole de messages. Chaque message devrait avoir un champ type, des données minimales et une signification claire. Évitez les messages trop génériques comme { refresh: true }, car ils deviennent difficiles à maintenir.

Pensez aussi au nettoyage. Lorsqu’un canal n’est plus utilisé, appelez close(). Dans les composants React, retirez les listeners au démontage, comme pour tout effet asynchrone. Cette discipline est similaire à celle que l’on applique avec AbortController pour nettoyer des effets ou annuler des requêtes.

Enfin, ne diffusez pas de secrets. Même si le canal est limité à la même origine, il ne doit pas transporter de tokens sensibles, de données personnelles inutiles ou d’informations que vous ne voudriez pas voir circuler dans l’ensemble de vos contextes frontend.

Limites à connaître

BroadcastChannel est simple, mais il n’est pas persistant. Si un onglet était fermé ou pas encore chargé au moment de l’envoi, il ne recevra pas l’événement. Pour un état durable, combinez-le avec localStorage, IndexedDB, une API serveur ou un cache correctement invalidé.

Il faut également prévoir un fallback si votre cible navigateur l’exige. Dans la plupart des applications modernes, le support est suffisant, mais une architecture robuste doit rester progressive. C’est le même état d’esprit que le progressive enhancement : l’expérience principale doit rester fonctionnelle, puis s’améliorer quand l’API est disponible.

const canUseBroadcastChannel = 'BroadcastChannel' in window;

if (canUseBroadcastChannel) {
  const channel = new BroadcastChannel('app');
  channel.postMessage({ type: 'ready' });
}

Pour aller plus loin, la documentation MDN sur BroadcastChannel reste une référence utile : BroadcastChannel sur MDN.

Conclusion

BroadcastChannel est une API discrète, mais très efficace pour améliorer la cohérence d’une application ouverte dans plusieurs onglets. Elle permet de synchroniser une déconnexion, propager un changement de thème, invalider un cache ou notifier une mise à jour locale sans ajouter de serveur temps réel.

Son intérêt principal tient à sa simplicité : un canal nommé, des messages typés, quelques listeners bien nettoyés. Utilisée avec mesure, elle devient une brique d’architecture frontend précieuse pour rendre vos interfaces plus prévisibles et plus agréables à utiliser.