Synchroniser un panier d’achat entre les onglets du navigateur : BroadcastChannel vs localStorage
Pourquoi les événements de stockage localStorage transmettent des données identiques, pourquoi les solutions basées sur Date.now ne fonctionnent pas de manière fiable, et comment BroadcastChannel combiné à un stockage durable résout le problème des badges de panier entre onglets.
Les badges de panier entre onglets semblent être une tâche de cinq minutes, jusqu’à ce qu’un ticket de contrôle qualité prouve que l’idée initiale envoie des messages en silence. La démonstration ci-dessous suit un scénario d’entretien courant : onglets de même origine, pas de mémoire partagée, pas de ping vers le serveur — uniquement des outils de la plateforme navigateur.
Scénario
Un acheteur garde deux onglets de produits ouverts. Il ajoute un article dans l’onglet A. Le badge en tête de l’onglet B doit être mis à jour en conséquence. Sinon, l’onglet B affiche toujours le vieux comptage et l’acheteur suppose que l’ajout a échoué.
Contraintes imposées par l’entreteneur : les onglets ne peuvent pas partager leurs tas JavaScript, et un aller-retour réseau est interdit pour le signal lui-même. C’est la plateforme qui doit transmettre l’information.
Première tentative : événements de stockage
La plupart des candidats recourent à localStorage ainsi qu’au écouteur storage. Une écriture dans un onglet notifie les autres onglets de la même origine.
// Tab that adds the item
function addToCart(sku) {
cart.add(sku);
renderBadge();
localStorage.setItem('cart-sync', JSON.stringify({
type: 'CART_ADD', sku, qty: 1
}));
}
// Every other tab
addEventListener('storage', (e) => {
if (e.key !== 'cart-sync') return;
const msg = JSON.parse(e.newValue);
if (msg.type === 'CART_ADD') { cart.add(msg.sku); renderBadge(); }
});
Ce schéma fonctionne généralement sans problème. Puis arrive la demande de correction.
Le rapport de QA qui provoque le problème
Reproduction : ajouter deux fois le même SKU. La fenêtre A affiche une quantité de 2, tandis que la fenêtre B reste à 1. Après un rechargement de la fenêtre B, celle-ci affiche enfin 2.
Rien ne plante. Les journaux sont calmes. Le système de surveillance semble correct — alors pourquoi le pair n’a-t-il pas reçu la mise à jour ?
Le indice réside dans le fait qu’un renouvellement corrige le problème : l’état durable était correct ; seul le signal en temps réel a échoué.
Cause racine : les valeurs identiques sont ignorées
L’algorithme HTML setItem abandonne son traitement lorsque la chaîne reçue correspond à ce qui est déjà stocké pour cette clé — selon la spécification, « si la valeur précédente est égale à la nouvelle valeur, arrêter ». Aucune écriture sur disque, aucune diffusion, aucun réveil du pair.
Des actions identiques dans le panier peuvent se traduire par les mêmes octets :
{"type":"CART_ADD","sku":"SKU-1029","qty":1}
Tab A continue d’être enregistré localement après son propre appel à add. Tab B ne reçoit jamais d’événement storage. Après un rechargement, tab B relit les données de stockage et semble fonctionner normalement — un problème classique intermittent en phase de test.
Pourquoi les doublons semblent rares mais ne le sont pas
Les séquences qui alternent les valeurs (A, puis B, puis A) continuent de fonctionner correctement. Les problèmes apparaissent avec des données identiques et consécutives :
- Doublons de clics ajoutant le même SKU
- Signaux périodiques publiant à nouveau un statut
"ONLINE"inchangé - Avertissements répétés de
SESSION_EXPIREDtandis qu’une page lente est encore en chargement
Ces sont précisément les moments où les systèmes ont le plus besoin d’un signal d’alerte.
La « unicité » des timestamps est fragile
Un correctif courant insère Date.now() dans le JSON afin que les chaînes de caractères diffèrent :
localStorage.setItem('cart-sync', JSON.stringify({
type: 'CART_ADD', sku, qty: 1,
t: Date.now() // force the value to differ
}));
Les horloges en millisecondes entrent en conflit lorsque deux écritures ont lieu au même instant. Le fait que le correctif fonctionne dépend alors de la chance du planificateur : parfois oui, parfois non. Une correction qui dépend de la granularité d’une horloge ordinaire n’est pas une véritable correction. Préférez crypto.randomUUID() (ou un autre token unique fiable) lorsque vous devez rester sur le stockage.
Le nettoyage entraîne une double livraison
Laisser des charges utiles uniques indéfiniment est problématique, c’est pourquoi les utilisateurs appellent immédiatement removeItem après setItem. Cela génère deux notifications : une pour l’écriture, une pour la suppression.
event 1 → { key:'cart-sync', oldValue: null, newValue: '{"type":"CART_ADD",…}' }
event 2 → { key:'cart-sync', oldValue: payload, newValue: null }
Sauf si le gestionnaire ignore newValue === null, le pair applique la mutation du panier deux fois. La conception où le stockage sert de bus nécessite désormais l’unicité, des vérifications pour les valeurs nulles, du parsing et un nettoyage — car l’API est un stockage clé/valeur qui émet parfois des messages, et non une file d’attente de messages.
Outil préféré : BroadcastChannel
Lorsque l’objectif est la messagerie et non le stockage persistant, utilisez l’API de messagerie :
const bus = new BroadcastChannel('cart-sync');
// send — the same message, as many times as you like
bus.postMessage({ type: 'CART_ADD', sku, qty: 1 });// receive
bus.onmessage = (e) => {
if (e.data.type === 'CART_ADD') { cart.add(e.data.sku); renderBadge(); }
};addEventListener('pagehide', () => bus.close());
Les objets identiques répétés sont tout de même transmis. Aucun raccourci basé sur l’égalité n’est appliqué. Le clonage structuré prend en charge des types plus riches que JSON (Date, Map, Set, tableaux typés). Considérez le stockage comme une base de données avec des mises à jour optionnelles, et BroadcastChannel comme un mécanisme de communication sans base de données.
Questions importantes en environnement de production
Auto-envoi. L’objet du canal de publication ne reçoit pas son propre message, mais une autre instance de canal portant le même nom dans le même document le reçoit — tout comme les iframes de même origine. Éliminez les doublons si plusieurs abonnés existent sur une même page.
Synchro vs asynchrone. La livraison est mise en file d’attente dans le cycle d’événements du destinataire (asynchrone). La clonage lors de postMessage est synchrone et rejette immédiatement les valeurs non clonables (DataCloneError pour les fonctions).
Fenêtres ouvertes tardivement. Les canaux ne rejouent pas l’historique. Une fenêtre ouverte après l’ajout affiche toujours l’ancien badge à moins qu’elle ne consulte un stockage durable. Schéma proposé :
// The shape that actually ships:
// durable store = the truth, channel = the doorbell
async function addToCart(sku) {
await idbPut('cart', sku); // atomic, survives reloads
bus.postMessage({ type: 'CART_CHANGED' }); // just a signal
}
bus.onmessage = async () => renderBadge(await idbCount('cart'));
Les données durables représentent la vérité ; le canal n’est qu’une sonnette d’alarme indiquant que cette vérité a changé.
Compteur dans le stockage. La lecture, la modification et l’écriture entre fenêtres entraînent la perte des mises à jour ; la plateforme ne propose aucun mécanisme de verrouillage. Ne créez pas de compteur distribué sur localStorage.
Là où le stockage reste supérieur. Les préférences telles que le thème ou la région, qui doivent être lues de manière synchrone avant l’affichage et changent rarement, s’adaptent mieux au stockage. Utilisez les événements storage pour les valeurs ; utilisez BroadcastChannel pour les événements.
Vérification personnelle
Avec l’approche naïve de stockage à charge égale, combien d’événements pairs sont générés par deux annonces identiques diffusées avec une seconde d’écart ?
localStorage.setItem('cart-sync', '{"sku":"SKU-1029"}');
// … one second later, same product added again …
localStorage.setItem('cart-sync', '{"sku":"SKU-1029"}');
Réponse : un seul événement. Le temps écoulé est sans importance ; seule une chaîne modifiée déclenche une notification.
Conclusion de l’interview
Privilégiez BroadcastChannel pour les signaux entre onglets, conservez IndexedDB ou un système similaire comme source fiable des données du panier, et citez le retour précoce en cas d’égalité de valeurs si l’idée de stockage en tant que bus est proposée. Mentionnez les limites de rediffusion entre onglets, l’effet d’écho multi-canal, ainsi que les raisons pour lesquelles les compteurs sans verrouillage dans localStorage échouent.
Points clés
La synchronisation des interfaces multi-onglets est en réalité un problème de messagerie déguisé en problème de stockage. Traitez l’état persistant et les notifications comme des couches distinctes, choisissez des API adaptées à chaque couche, et testez des séquences d’actions identiques — les tutoriels ne prennent pas en compte ces cas, tandis que les utilisateurs en production doivent effectuer des double-clics.
Notes supplémentaires pour la production
Les navigateurs mobiles peuvent supprimer de manière agressive les onglets en arrière-plan ; une mise à jour du badge lors du événement visibilitychange qui relit le stockage persistant permet de gérer les cas où un message a été manqué en raison du gel de l’application. Associez cela au canal de communication pour les utilisateurs en premier plan.
Les tests automatisés doivent fonctionner dans deux contextes (onglets Playwright) et vérifier à la fois que le stockage conserve les mêmes données et que BroadcastChannel transmet bien les duplicatas. Documentez le schéma de clés persistantes choisi afin qu’une synchronisation future sur serveur puisse s’effectuer sans devoir créer une deuxième source de vérité.
Les flags fonctionnelles bloquent parfois le comportement des « badges en temps réel ». La mise à jour permanente doit rester inconditionnelle ; seul l’élément de notification peut être optionnel. Sinon, une tab avec le flag désactivé s’éloignera définitivement des tabs avec le flag activé.
Sur le plan de la sécurité, ne mettez jamais de tokens d’authentification dans les messages de localStorage. Les codes SKU des paniers sont acceptables ; en revanche, les secrets de session ne le sont pas. Préférez envoyer des identifiants opaques et permettez à chaque tab de lire les détails privilégiés via des canaux httpOnly ou de la mémoire, après une vérification de session validée.
Si le site commercial comprend plusieurs sous-domaines, BroadcastChannel ne pourra pas les relier entre eux. Les solutions possibles incluent un worker partagé par le premier parti sur un domaine parent commun, ou des événements envoyés depuis le serveur identifiés par le code du panier. Soulignez cette limitation dès les premières réunions de conception.
L’internationalisation des badges (règles au pluriel) doit s’exécuter dans chaque tab après lecture du compteur ; ne diffusez pas de chaînes préformatées à moins que toutes les localisations ne soient garanties identiques.
Finalement, mesurez : enregistrez la fréquence à laquelle des ajouts de SKU dupliqués apparaissent dans les analyses. Si ce taux est significatif, le piège du stockage à valeur égale aurait constitué un incident de production latent, attendant le premier utilisateur avancé utilisant plusieurs onglets.
Association de l’entretien à un document de conception
Lorsque vous rédigez ce contenu pour une équipe, identifiez trois décisions distinctes : (1) quel stockage durable contiendra les informations des paniers, (2) quel mécanisme de transmission activera les onglets pairs, et (3) comment le réducteur de badges interprétera les messages reçus. Réduire ces décisions à « utilisez simplement localStorage » est ce qui crée le piège de l’égalité.
Liste de vérification des tests
- Ajouter deux fois un SKU identique avec une sonnette ne stockant que des données (attendre une erreur)
- Ajouter deux fois en utilisant BroadcastChannel (attendre deux mises à jour)
- Ouvrir une troisième fenêtre après les ajouts (attendre un comptage correct provenant d’une lecture fiable, et non d’une répétition)
- Ajouter rapidement plusieurs fois en une milliseconde avec des timestamps uniques (attendre des erreurs intermittentes)
- Utiliser setItem + removeItem sans vérification pour éviter les valeurs nulles (attendre un double comptage)
L’automatisation de cette liste de vérification dans les tests d’intégration permet d’éviter les dérives lorsque quelqu’un décide de revenir à des événements basés uniquement sur le stockage.
Pourquoi les interviewers aiment cette question
Cela récompense la lecture des spécifications plutôt que la mémorisation des noms d’API. Les candidats qui se sont seulement contentés de parcourir MDN manquent l’information concernant la valeur de retour liée à l’égalité. Ceux qui ont développé des interfaces multi-onglets mentionnent BroadcastChannel et les stores durables sans avoir besoin d’indications. Les questions supplémentaires sur les onglets en retard ou les compteurs sans verrou permettent de déterminer si la réponse provient d’un extrait de blog ou d’une expérience concrète.
Pour les versions à emporter, demandez un petit répertoire de démonstration contenant deux routes ainsi qu’un README décrivant les couches choisies. Les examinateurs devraient ouvrir deux fenêtres et cliquer : une démonstration manuelle vaut mieux qu’un paragraphe théorique.
Modèles associés
Les indicateurs de présence, les curseurs collaboratifs (légers) et la fonction de déconnexion utilisent le même modèle Doorbell. L’édition collaborative de documents nécessite généralement des CRDT ou un serveur ; ne forcez pas BroadcastChannel à remplir le rôle d’un protocole de cohérence. Restez fidèles à l’exemple du panier : synchronisation des badges de manière éventuelle, panier fiable et durable, avec possibilité de réconciliation via un serveur ultérieurement.
Gardez le fonctionnement en production simple : un seul panier durable, une seule instance Doorbell, des tests explicites pour détecter les actions dupliquées, et sans recours astucieux à des horloges mesurant les millisecondes. C’est cette simplicité qui permet aux badges multi-onglets de fonctionner correctement même lorsque les utilisateurs ouvrent plus de fenêtres que ce qui était montré dans la démonstration standard.
Cette combinaison suffit. C’est terminé.