Persistencia del dato: no solo de cookies vive el hombre
Más allá de las cookies: una guía completa para la persistencia de datos en el cliente y el servidor
En este artículo, exploraremos cómo utilizar sessionStorage, localStorage, indexedDB y las APIs propias para que los datos persistan, y cómo podemos valernos de otros recursos, como postMessage o broadcastChannel, para comunicarnos de forma más efectiva y ofrecer así una mejor experiencia de usuario
Cuando hablamos de persistencia del dato en aplicaciones web, inmediatamente pensamos en nuestras amigas las cookies, ¿o no? Sin embargo, existen otras herramientas esenciales para mantener la información disponible durante la navegación del usuario, más allá de estas. En este artículo, exploraremos cómo utilizar sessionStorage, localStorage, indexedDB y las APIs propias para que los datos persistan, y cómo podemos valernos de otros recursos, como postMessage o broadcastChannel, para comunicarnos de forma más efectiva y ofrecer así una mejor experiencia de usuario. Con esta guía, intentaré mostrarte los matices, pros y contras de cada método y cómo se implementan en un contexto de eCommerce y de interacción en tiempo real.
SessionStorage y LocalStorage: persistencia clásica en el cliente
sessionStorage y localStorage son APIs de almacenamiento web que ofrecen diferentes niveles de persistencia en el cliente. Disponibles en cualquier navegador, ambos métodos son extremadamente útiles, no solo en el contexto de la capa de datos, sino para el propio funcionamiento del sitio web, permitiéndote gestionar el estado de la aplicación a medida que el usuario interactúa con el sitio.
SessionStorage
sessionStorage almacena datos que solo deben persistir durante la sesión del usuario y desaparecen al cerrar la pestaña. Este almacenamiento es perfecto para manejar datos temporales como el estado de un carrito de compras o las preferencias de visualización, por ejemplo.
Ejemplo práctico: Almacenamiento temporal de estado de carrito en JavaScript
// Guardar el estado del carrito en sessionStorage sessionStorage.setItem("cartItems", JSON.stringify(cartItems));
// Recuperar datos del carrito al cambiar de página en la misma sesión const cartData = JSON.parse(sessionStorage.getItem("cartItems")); console.log("Datos del carrito:", cartData); LocalStorage
localStorage, en cambio, permite que los datos persistan incluso después de que el usuario cierre el navegador, ideal para almacenar configuraciones a largo plazo como temas de usuario o tokens de autenticación.
Ejemplo práctico: Guardar la preferencia de tema de usuario en JavaScript
// Almacenar la preferencia de tema localStorage.setItem("theme", "dark");
// Recuperar el tema seleccionado al cargar la página const userTheme = localStorage.getItem("theme");document.body.classList.add(userTheme);Consideraciones de seguridad: Ambos métodos pueden ser susceptibles a ataques XSS. Evita almacenar datos sensibles y utiliza solo scripts de fuentes confiables.
IndexedDB: persistencia avanzada
indexedDB es una API de bajo nivel —esto quiere decir que proporciona acceso directo a todos los recursos, pero requiere de un conocimiento profundo sobre la API en cuestión para sacarle el máximo provecho— que ofrece acceso a una base de datos no relacional en el navegador, permitiéndote almacenar grandes volúmenes de datos en formato JSON. Es ideal para aplicaciones complejas y de alto rendimiento, como aquellas en el ámbito del eCommerce y la analítica, donde almacenar localmente el estado completo de objetos (por ejemplo, un producto o el carrito de compras) puede optimizar la experiencia del usuario y reducir la dependencia del servidor, ya que no bloquea el comportamiento del sitio web.
¿Cómo funciona IndexedDB?
IndexedDB ofrece acceso tanto síncrono como asíncrono, aunque yo siempre que lo he utilizado ha sido con su API asíncrona. Funciona mediante una interfaz basada en eventos, con una API predominantemente asíncrona para que las operaciones de lectura y escritura no bloqueen la interfaz de usuario. Si bien no es nativamente una API de promesas, es posible envolver sus operaciones en promesas para aprovechar una sintaxis más moderna y limpia, compatible con async y await.

Este sistema de base de datos maneja transacciones y objetos clave-valor, permitiendo estructuras de datos complejas y multitabla. Además, IndexedDB permite establecer índices para realizar búsquedas optimizadas, lo que facilita consultas avanzadas dentro de la base de datos. Sin embargo, esta API es compleja y tiene ciertas limitaciones, como su dependencia en el almacenamiento local del navegador y la falta de sincronización entre dispositivos (aunque puedes combinarlo con otros métodos para conseguir cosas muy chulas).
Ejemplo práctico: Almacenamiento y recuperación de datos en IndexedDB
Aquí te muestro un ejemplo básico en JavaScript que utiliza promesas para gestionar una base de datos de productos en IndexedDB. Este ejemplo crea o abre una base de datos, añade un producto y lo recupera para mostrarlo en la consola.
// Función para abrir la base de datos
function openDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open("ecommerceDB", 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
db.createObjectStore("products", { keyPath: "productId" });
};
request.onsuccess = (event) => resolve(event.target.result);
request.onerror = (event) => reject("Error al abrir la base de datos");
});
}
// Función para añadir un producto
function addProduct(db, product) {
return new Promise((resolve, reject) => {
const transaction = db.transaction("products", "readwrite");
const store = transaction.objectStore("products");
const request = store.put(product);
request.onsuccess = () => resolve("Producto añadido con éxito");
request.onerror = () => reject("Error al añadir el producto");
});
}
// Función para recuperar un producto por ID
function getProduct(db, productId) {
return new Promise((resolve, reject) => {
const transaction = db.transaction("products", "readonly");
const store = transaction.objectStore("products");
const request = store.get(productId);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject("Error al recuperar el producto");
});
}Este código ilustra cómo utilizar promesas para hacer que las operaciones en IndexedDB sean más intuitivas y sencillas de leer. La API permite ejecutar operaciones de transacción y lectura/escritura de manera asíncrona, optimizando el rendimiento de aplicaciones con grandes cantidades de datos.
Ventajas y limitaciones de IndexedDB
Ventajas: IndexedDB permite almacenar grandes volúmenes de datos y realizar consultas complejas de forma local, siendo ideal para aplicaciones que manejan estructuras de datos complejas en el lado del cliente.
Limitaciones: Al tratarse de una API de bajo nivel, su uso requiere una mayor inversión en desarrollo y conocimiento técnico. Además, es limitada en cuanto a sincronización entre dispositivos y puede tener lagunas técnicas al compararla con bases de datos de servidor.
Consejo avanzado: Herramientas como notificaciones push se apoyan en IndexedDB para almacenar datos del usuario de forma local, asegurando que las notificaciones sean relevantes sin tener que hacer llamadas al servidor en cada carga de página.
Personalmente, la he utilizado para mantener la persistencia del producto en formato array de objeto —vamos, el clásico formato items de GA4 con todas sus variables y casi media docena de variables custom— desde que se añadía el producto al carrito hasta que finalizaba su compra; con toda la lógica de añadir más unidades, eliminarlas, así como eliminar el producto de forma definitiva si finalizaba la compra. Además, gracias a su persistencia entre sesiones y su capacidad de almacenamiento (que varía entre navegadores, pero para que te hagas una idea, en Chrome tienes hasta 500 MB de espacio), te permite incluso recuperar carritos al iniciar sesión… Ahí es nada ;)
function getIndexedDBItem() {
var openRequest = indexedDB.open("XXX", 1);
openRequest.onsuccess = function() {
var db = openRequest.result;
var products = [];
var oldItems = google_tag_manager["GTM-XXXXX"].dataLayer.get("cdl_cart_products");
if (oldItems && oldItems.length > 1) {
var transaction = db.transaction(["Products"], "readonly");
var store = transaction.objectStore("Products");
oldItems.forEach(function(oldItem) {
var request = store.get(oldItem.sku);
request.onsuccess = function(e) {
var productData = e.target.result;
if (productData) {
products.push({
'item_id': productData.product_id || productData.sku,
'item_name': productData.name || productData.product_name,
'item_brand': productData.brand || "N/A",
'item_category': productData.category1 || 'not_set',
'item_category2': productData.category2 || '',
'item_category3': productData.category3 || '',
'quantity': oldItem.quantity,
'price': parseFloat(oldItem.unitprice_ati * oldItem.quantity),
'discount': parseFloat(productData.discount) ? parseFloat(productData.discount * oldItem.quantity) : 0,
'deliveryHomeAvailability': productData.dimension148,
'deliverySFSAvailability': productData.dimension149,
'deliveryStoreAvailability': productData.dimension150,
'productRating': productData.rating || undefined,
'productNumReviews': productData.number_reviews || undefined,
'item_list_id': productData.zone_id || undefined,
'item_list_name': productData.zone_name || undefined
});
}
};
});
transaction.oncomplete = function() {
dataLayer.push({
'event': 'funnelEcommerceGA4',
'funnelItemsGA4': products
});
};
}
};
openRequest.onerror = function() {
console.error("Error al abrir la base de datos:", openRequest.error);
};
}
getIndexedDBItem();APIs Propias: persistencia en el servidor
Además de los métodos de persistencia en el cliente, el uso de APIs propias ofrece una forma de persistencia en el servidor, conectándose a bases de datos alojadas en la nube, por ejemplo. Este enfoque asegura que los datos persistan de manera segura y puedan compartirse entre dispositivos y sesiones, ideal para casos de uso donde se necesita centralizar y gestionar grandes volúmenes de datos.

Las APIs propias son especialmente útiles en contextos de analítica avanzada o personalización. Si nos abstraemos un poco, hasta sistemas como GTM Server-Side pueden actuar como sistemas de persistencia de datos al hacer de intermediarios entre el cliente y la base de datos, facilitando el almacenamiento en tiempo real sin afectar el rendimiento del sitio. Esto permite recopilar información en el servidor y procesarla de manera centralizada, minimizando la carga en el cliente y ofreciendo persistencia multidispositivo.
Ejemplo práctico: Persistencia con API en Firestore y TMS Server-Side
Imaginemos que queremos almacenar y recuperar los datos de usuario en tiempo real:
Recopilación en el cliente: El cliente dispara un evento, enviando datos a nuestro sistema de tagging Server-Side.
Almacenamiento en Firestore: Nuestro TMS procesa y guarda los datos en Firestore.
Recuperación a través de la API: Otra API en el servidor consulta los datos en tiempo real, permitiendo su uso en analítica o personalización de la experiencia del usuario.
Código ilustrativo en python FastAPI para acceder a datos en Firestore:
from fastapi import FastAPI
from google.cloud import firestore
app = FastAPI()
db = firestore.Client()
@app.get("/get_user_data/{user_id}")
async def get_user_data(user_id: str):
doc_ref = db.collection("users").document(user_id)
doc = doc_ref.get()
if doc.exists:
return doc.to_dict()
else:
return {"error": "User data not found"}Con esta API, los datos de usuario se almacenan en Firestore y se acceden en tiempo real, permitiendo persistencia multidispositivo y sincronización entre sesiones. Imagínate la de posibilidades que se te abren con una solución que funcione en el lado servidor…
Comunicación en tiempo real: BroadcastChannel y postMessage
Además de los métodos de almacenamiento, existen alternativas potentes para “persistencia temporal” y comunicación entre diferentes contextos de navegación: broadcastChannel y postMessage. Aunque no se trata de APIs y métodos desarrollados para persistir dato, sí se pueden utilizar y combinar con los métodos mencionados anteriormente de persistencia bajo determinadas circunstancias…
BroadcastChannel: Comunicación entre pestañas del mismo dominio

broadcastChannel es una API que permite que diferentes pestañas de una misma aplicación compartan información en tiempo real. Es ideal para sincronizar el estado de autenticación o los datos del carrito en distintas pestañas. Esto resulta clave en experiencias de usuario que requieren que las acciones en una pestaña se reflejen en otra.
Ejemplo práctico: Sincronización de autenticación entre pestañas en JavaScript
// Crear un canal para la autenticación
const authChannel = new BroadcastChannel("authChannel");
// Enviar un mensaje de autenticación desde una pestaña
authChannel.postMessage({ event: "login", userId: "12345" });
// Escuchar el mensaje en otra pestaña
authChannel.onmessage = (event) => {
if (event.data.event === "login") {
console.log("Usuario autenticado en otra pestaña:", event.data.userId);
// Actualizar el estado de la sesión o capa de datos
}
};Yo lo he utilizado en aquellos eCommerce que, debido a la lógica de programación del sitio web, se abrían pestañas nuevas de otras páginas para continuar el proceso de compra. De esta manera, era capaz de registrar las transacciones en el propio eCommerce padre, aunque el usuario lo hiciera desde otra página (incluso con dominio diferente). La magia de la persistencia una vez más en acción.
//Canal para recibir mensajes de pestañas
const transactionChannel = new BroadcastChannel("transactionChannel");
transactionChannel.onmessage = (event) => {
if (event.data.event === "transactionComplete") {
const transactionData = event.data.transactionDetails;
console.log("Transacción completada recibida:", transactionData);
dataLayer.push({
'event': 'purchase',
'transactionDetails': transactionData
});
}
};
//Enviar mensajes al dominio padre
const transactionChannel = new BroadcastChannel("transactionChannel");
function completeTransaction(transactionDetails) {
// Envía los datos de la transacción al canal
transactionChannel.postMessage({
'event': 'transactionComplete',
'transactionDetails': transactionDetails
});
console.log("Transacción completada y enviada al canal");
}
const transactionDetails = {
'transaction_id': '12345ABC',
'value': 100.50,
'currency': 'USD',
'items': [
{
'item_id': '001',
'item_name': 'Producto A',
'quantity': 1,
'price': 50.25
},
{
'item_id': '002',
'item_name': 'Producto B',
'quantity': 1,
'price': 50.25
}
]
};
completeTransaction(transactionDetails);PostMessage: Comunicación entre ventanas y iframes
El método postMessage permite la transferencia segura de datos entre ventanas o iframes de distintos dominios, lo cual es invaluable cuando integras servicios externos como pasarelas de pago. Este método facilita que el estado de transacciones y otras interacciones dentro de un iframe se comuniquen con la ventana principal.
Ejemplo práctico: Enviar el estado de la transacción desde un iframe de pago en JavaScript
// Enviar un mensaje desde el iframe al sitio principal
iframe.contentWindow.postMessage({ event: "paymentSuccess", transactionId: "abc123" }, "*");
// Escuchar y procesar el mensaje en el sitio principal
window.addEventListener("message", (event) => {
if (event.origin === "https://trusted-payment.com") {
console.log("Estado de la transacción recibido:", event.data.transactionId);
// Lógica para actualizar la capa de datos o registrar el evento
}
});Nota de seguridad: Siempre valida
event.originpara asegurarte de que los datos provienen de una fuente confiable antes de procesarlos. Más vale prevenir que curar, créeme.
Mi solución de cabecera para comunicarme con pasarelas de pago desde el DOM y almacenar información en la capa de datos de forma cómoda y —sobre todo— segura.
window.addEventListener("message", function (event) {
if (event.origin === "https://js.stripe.com") {
try {
var data = JSON.parse(event.data);
if (
data.message &&
data.message.payload &&
data.message.payload.response &&
data.message.payload.response.object &&
data.message.payload.response.object.status
) {
if (
data.message.payload.response.object.status === "requires_capture"
) {
digitalData.events.push("postMessage-stripe_gateway");
dataLayer.push({
event: "gateway_confirmation",
digitalData: digitalData,
});
}
}
} catch (error) {
console.debug("Error procesando el mensaje de Stripe:", error);
}
}
});Comparativa de métodos de persistencia
Cada “método de persistencia” comentado aquí tiene sus características, fortalezas y limitaciones. La siguiente tabla te ayuda a identificar cuál elegir en función del tipo de dato y la duración deseada:
Al combinar todos estos métodos, tienes a tu disposición un abanico completo de opciones para asegurar que el dato, al igual que la experiencia de usuario, persista de la mejor manera.
Adáptate a las necesidades del sitio web
La persistencia del dato es una pieza fundamental en la experiencia de usuario moderna. Al entender y aplicar estos métodos en función de la necesidad específica de tu aplicación, puedes garantizar que los datos relevantes estén disponibles cuando el usuario los necesite, mejorando tanto el rendimiento como la consistencia de la experiencia. Al combinar todos estos métodos, tienes a tu disposición un abanico completo de opciones para asegurar que el dato, al igual que la experiencia de usuario, persista de la mejor manera.
¿Y a ti, se te ocurre algún otro?







¡Gracias por el artículo! 🙏🙏🙏