Sincronizar un carrito de compras entre pestañas del navegador: BroadcastChannel vs localStorage
Por qué los eventos de almacenamiento de localStorage envían cargas idénticas, por qué las soluciones con Date.now son inestables, y cómo BroadcastChannel junto con un almacenamiento duradero resuelven los problemas de las insignias del carrito entre pestañas.
Los distintivos de la cesta entre pestañas parecen una tarea de cinco minutos hasta que un ticket de QA demuestra que la primera idea no envía mensajes de forma silenciosa. La guía a continuación sigue un escenario de entrevista común: pestañas del mismo origen, sin memoria compartida, sin ping al servidor; solo herramientas de la plataforma del navegador.
Escenario
Un comprador mantiene abiertas dos pestañas de productos. Agrega un artículo en la pestaña A. El distintivo del encabezado de la pestaña B debería actualizarse al instante. De lo contrario, la pestaña B sigue mostrando el conteo anterior y el comprador asume que la adición falló.
Restricciones impuestas por el entrevistador: las pestañas no pueden compartir montones de JavaScript, y está prohibido realizar un viaje de ida y vuelta por red para enviar la señal en sí. La plataforma debe encargarse de transmitir el aviso.
Primer intento: eventos de almacenamiento
La mayoría de los candidatos recurren a localStorage junto con el listener de storage. Una escritura en una pestaña notifica a las demás pestañas del mismo origen.
// 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(); }
});
Ese boceto suele funcionar sin problemas. Luego llega el ticket.
El informe de QA que lo rompe
Reproducción: se agrega el mismo SKU dos veces. La pestaña A muestra la cantidad 2. La pestaña B permanece en 1. Al volver a cargarla, finalmente muestra 2.
Nada falla. Los registros están en silencio. El receptor parece correcto; entonces, ¿por qué el otro dispositivo no recibió la actualización?
La pista es que una actualización corrige el problema: el estado duradero era correcto; solo la señal en tiempo real falló.
Causa raíz: se ignoran los valores iguales
El algoritmo HTML setItem abandona su proceso cuando la cadena recibida coincide con lo que ya está almacenado para esa clave; según la especificación, “si el valor anterior es igual al nuevo, detenerse”. No hay escritura en disco, ni transmisión, ni activación del otro dispositivo.
Acciones idénticas en el carrito pueden serializarse en los mismos bytes:
{"type":"CART_ADD","sku":"SKU-1029","qty":1}
La pestaña A sigue escribiendo localmente después de su propio add. La pestaña B nunca recibe un evento storage. Después de volver a cargar, la pestaña B vuelve a leer el almacenamiento y parece estar bien: un problema intermitente clásico en las pruebas de calidad.
Por qué los duplicados parecen raros pero no lo son
Las secuencias que alternan valores (A luego B luego A) siguen funcionando. El problema aparece con cargas únicas idénticas consecutivas:
- Doble clics que añaden el mismo SKU
- Señales de actividad que vuelven a publicar un estado
"ONLINE"sin cambios - Avisos repetidos de
SESSION_EXPIREDmientras una pestaña lenta sigue iniciándose
Esos son exactamente los momentos en que los equipos necesitan urgentemente una alerta.
La “unicidad” de las marcas de tiempo es frágil
Un parche común inserta Date.now() en el JSON para que las cadenas sean diferentes:
localStorage.setItem('cart-sync', JSON.stringify({
type: 'CART_ADD', sku, qty: 1,
t: Date.now() // force the value to differ
}));
Los relojes de milisegundos entran en conflicto cuando dos escrituras ocurren en el mismo milisegundo. Que la solución funcione entonces depende de la suerte del planificador: a veces sí, otras veces no. Una corrección que depende de la granularidad del reloj de pared no constituye verdadera corrección. Prefiera crypto.randomUUID() (u otro token único fiable) cuando sea necesario permanecer en el almacenamiento.
La limpieza genera entregas duplicadas
Dejar payloads únicos de forma permanente resulta problemático, por lo que la gente llama a removeItem inmediatamente después de llamar a setItem. Esto emite dos notificaciones: una para la escritura y otra para la eliminación.
event 1 → { key:'cart-sync', oldValue: null, newValue: '{"type":"CART_ADD",…}' }
event 2 → { key:'cart-sync', oldValue: payload, newValue: null }
A menos que el manejador ignore la condición newValue === null, el dispositivo remoto aplica la mutación del carrito dos veces. El diseño que utiliza el almacenamiento como bus ahora requiere garantías de unicidad, protecciones contra valores nulos, procesamiento de datos y tareas de limpieza, ya que la API funciona como un almacén clave/valor que emite mensajes ocasionalmente, y no como una cola de mensajes.
Herramienta preferida: BroadcastChannel
Cuando el objetivo es enviar mensajes y no almacenarlos de forma persistente, utilice la API de mensajería:
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());
Los objetos idénticos repetidos siguen funcionando. No hay atajo por igualdad. El clon estructurado permite tipos más avanzados que JSON (Date, Map, Set, arrays tipados). Considere el almacenamiento como una base de datos con posibles notificaciones de cambios, y BroadcastChannel como una notificación sin base de datos.
Preguntas importantes para su uso en producción
Entrega automática. El objeto del canal de publicación no recibe su propio mensaje, pero otra instancia del canal con el mismo nombre en el mismo documento sí lo hace; lo mismo ocurre con los iframes del mismo origen. Elimine duplicados si hay varios suscriptores en una misma página.
Sincrónico vs. asíncrono. La entrega se coloca en la cola del bucle de eventos del receptor (asíncrono). La clonación durante postMessage es síncrona y rechaza de inmediato los valores no clonables (DataCloneError para funciones).
Pestañas abiertas tarde. Los canales no reproducen el historial. Una pestaña abierta después de la adición sigue mostrando la insignia antigua a menos que consulte un almacenamiento duradero. Patrón utilizado:
// 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'));
Los datos duraderos son la verdad; el canal es solo la campanilla que anuncia que esa verdad ha cambiado.
Contador en el almacenamiento. Leer, modificar o escribir entre pestañas hace que se pierdan las actualizaciones; la plataforma no ofrece mecanismos de bloqueo. No intente crear un contador distribuido en localStorage.
Aquí donde el almacenamiento sigue siendo la mejor opción. Las preferencias como el tema o la configuración regional que deben leerse de forma síncrona antes de la renderización rara vez cambian. Utilice los eventos storage para los valores; use BroadcastChannel para los eventos.
Autoverificación
Con el enfoque ingenuo de almacenamiento con carga igual, ¿cuántos eventos entre pares se generan al añadir dos elementos idénticos con un segundo de diferencia?
localStorage.setItem('cart-sync', '{"sku":"SKU-1029"}');
// … one second later, same product added again …
localStorage.setItem('cart-sync', '{"sku":"SKU-1029"}');
Respuesta: un único evento. El tiempo transcurrido es irrelevante; solo una cadena modificada activa la notificación.
Cierre de la entrevista
Utilice BroadcastChannel para las señales entre pestañas, mantenga IndexedDB o similar como fuente de datos fiable del carrito, y mencione el retorno inmediato con valores iguales si se propone usar el almacenamiento como bus de comunicación. Hable también sobre los límites de reproducción en pestañas posteriores, el eco automático multi-canal y por qué los contadores sin bloqueos en localStorage fallan.
Consejos clave
La sincronización de la interfaz entre pestañas es un problema de mensajería disfrazado de funcionalidad de almacenamiento. Trate el estado duradero y las notificaciones como capas separadas, elija APIs que se adapten a cada capa y pruebe acciones consecutivas idénticas; los tutoriales suelen omitir estos casos, mientras que los usuarios en entorno de producción se enfrentan a problemas al hacer doble clic.
Notas adicionales para producción
Los navegadores móviles pueden descartar las pestañas en segundo plano de forma agresiva; actualizar la indicación visual en visibilitychange y volver a leer el almacenamiento duradero soluciona los casos en que se pierde un mensaje mientras la aplicación está congelada. Combine esto con el uso de BroadcastChannel para los usuarios en primer plano.
Las pruebas automatizadas deben ejecutarse en dos contextos (pestañas de Playwright) y verificar tanto que el almacenamiento almacene datos idénticos como que BroadcastChannel entregue las copias correspondientes. Documente el esquema de claves duraderas elegido para que una futura sincronización con el servidor pueda realizarse sin necesidad de crear una segunda fuente de verdad.
Las banderas de funcionalidad a veces controlan el comportamiento del “badge en vivo”. Mantenga la escritura permanente sin condiciones; solo la campanilla puede ser opcional. De lo contrario, una pestaña con la bandera desactivada se desviará permanentemente de las pestañas con la bandera activada.
Desde el punto de vista de la seguridad, nunca coloque tokens de autenticación en los mensajes de localStorage. Los códigos SKU del carrito están bien; los secretos de sesión, no. Prefiera enviar identificadores opacos y permitir que cada pestaña lea detalles privilegiados desde canales httpOnly o de la memoria después de una verificación de sesión validada.
Si la tienda online abarca varios subdominios, BroadcastChannel no podrá cruzarlos. Las opciones incluyen un worker compartido por el propio sitio en un dominio padre común, o eventos enviados desde el servidor con clave basada en el ID del carrito. Mencione esta limitación temprano en las revisiones de diseño.
La internacionalización del badge (reglas para el plural) debe realizarse en cada pestaña después de leer la cantidad; no difunda cadenas preformateadas a menos que se garantice que todas las localizaciones sean idénticas.
Finalmente, mida: registre con qué frecuencia ocurren las adiciones de SKU duplicados en los análisis. Si la tasa es significativa, la trampa del almacenamiento de valores iguales habría sido un incidente latente en producción que esperaba al primer usuario avanzado con varias pestañas.
Vinculación de la entrevista al documento de diseño
Al redactar esto para el equipo, separe tres decisiones: (1) qué almacén duradero contendrá las líneas del carrito, (2) qué mecanismo activará las pestañas asociadas y (3) cómo interpreta el reductor de insignias los mensajes del timbre. Reducir esas decisiones a “simplemente usar localStorage” es lo que genera la trampa de igualdad.
Un breve documento de diseño puede incluir un diagrama de secuencia: clic del usuario → modificación en IndexedDB → postMessage a través de BroadcastChannel → las pestañas asociadas invalidan la consulta de insignias. Tenga en cuenta los modos de fallo: canales no soportados (raro en navegadores modernos, pero verifique), peculiaridades del modo privado y navegadores con múltiples perfiles que aíslan el almacenamiento.
Lista de verificación de pruebas
- Añadir dos veces un SKU idéntico con timbre solo de almacenamiento (esperar error)
- Añadir dos veces usando BroadcastChannel (esperar dos actualizaciones)
- Abrir una tercera pestaña después de los añadidos (esperar conteo correcto a partir de la lectura duradera, no de la reproducción)
- Añadir rápidamente dentro de un milisegundo con unicidad de marca de tiempo (esperar errores intermitentes)
- setItem + removeItem sin protección contra valores nulos (esperar conteo duplicado)
Automatizar esa lista de verificación en CI evita regresiones cuando alguien “simplifica” el proceso a eventos de almacenamiento.
Por qué a los entrevistadores les gusta esta pregunta
Recompensa la lectura de las especificaciones, no el memorización de los nombres de las API. Los candidatos que solo han echado un vistazo a MDN pasan por alto el valor de retorno relacionado con la igualdad. Aquellos que han desarrollado interfaces con varias pestañas mencionan BroadcastChannel y almacenamientos duraderos sin necesidad de orientación adicional. Las preguntas posteriores sobre pestañas tardías y contadores sin bloqueo revelan si la respuesta proviene de un fragmento de artículo de blog o de experiencia real.
Para las versiones para llevar a casa, pida un pequeño repositorio de demostración con dos rutas y un README que describa las capas seleccionadas. Los evaluadores deben abrir dos ventanas e interactuar con el código; la prueba manual es mejor que un párrafo de teoría.
Patrones relacionados
Los indicadores de presencia, los cursores colaborativos (ligeros) y la funcionalidad de cierre de sesión utilizan el mismo modelo de doorbell. La edición colaborativa de documentos suele requerir CRDTs o un servidor; no intente convertir BroadcastChannel en un protocolo de consistencia. Mantenga el ejemplo del carrito con sinceridad: sincronización eventual de insignias, un carrito duradero y autoritativo, y una reconciliación con servidor opcional más adelante.
Mantenga la solución simple en producción: un carrito duradero, un doorbell, pruebas explícitas contra acciones duplicadas, y sin depender de relojes de milisegundos. Esa estructura sencilla es la que mantiene las insignias multi-pestaña fiables cuando los usuarios abren más ventanas de las que mostró la demostración estándar.
Esa combinación es suficiente. Listo.
Lecturas relacionadas
- Veinte preguntas en la entrevista sobre React que diferencian el uso de la comprensión — DOM virtual, claves, efectos, memorización, Contexto, SSR e hidratación, explicados con las dificultades reales que investigan los entrevistadores, y no con definiciones de libros de texto.
- Cómo los contextos de cierre de V8 conservan la memoria más allá de lo que usa una función — V8 crea un contexto compartido por llamada para cada variable externa mencionada por cualquier función interna; así, elementos no utilizados y listeners permanentes pueden retener valores de gran tamaño.