Storage y Cookies
Aprende a persistir datos en el navegador utilizando diferentes mecanismos de almacenamiento del lado del cliente.
Cristian Escalante
Última actualización: 23 de abril de 2025
Storage y Cookies
Persiste datos en el navegador:
- LocalStorage y SessionStorage
- Almacenamiento y recuperación de datos
- Cookies: creación, lectura y gestión
- Limitaciones de cada método de almacenamiento
- Seguridad en el almacenamiento del lado del cliente
- IndexedDB para almacenamiento complejo
- Cache API
- Sincronización con servidores
Introducción al almacenamiento en el navegador
El almacenamiento en el navegador permite persistir datos del lado del cliente, lo que posibilita:
- Guardar preferencias del usuario
- Almacenar datos de sesión
- Mejorar el rendimiento al cachear datos
- Habilitar funcionalidades offline
- Reducir peticiones al servidor
Existen diferentes mecanismos con distintos propósitos, capacidades y limitaciones:
// Ejemplo simple de almacenamiento
localStorage.setItem('preferencia', 'modo-oscuro');
const preferencia = localStorage.getItem('preferencia');
console.log(`Preferencia guardada: ${preferencia}`); // "Preferencia guardada: modo-oscuro"
Web Storage API
La API Web Storage proporciona dos mecanismos principales para almacenar datos en el navegador:
LocalStorage
Almacena datos sin fecha de expiración que persisten incluso después de cerrar el navegador.
// Guardar datos
localStorage.setItem('usuario', 'Juan');
localStorage.setItem('config', JSON.stringify({tema: 'oscuro', notificaciones: true}));
// Leer datos
const usuario = localStorage.getItem('usuario');
const config = JSON.parse(localStorage.getItem('config'));
// Eliminar un item específico
localStorage.removeItem('usuario');
// Eliminar todos los datos
localStorage.clear();
// Obtener el número de elementos
const numItems = localStorage.length;
// Acceder por índice
const clave = localStorage.key(0);
const valor = localStorage.getItem(clave);
SessionStorage
Similar a localStorage, pero los datos solo persisten durante la sesión actual del navegador.
// Guardar datos temporales de sesión
sessionStorage.setItem('carrito', JSON.stringify([{id: 1, nombre: 'Producto 1'}]));
// Leer datos
const carrito = JSON.parse(sessionStorage.getItem('carrito'));
// Los datos se eliminan automáticamente al cerrar la pestaña o ventana
Limitaciones de Web Storage
- Capacidad limitada (generalmente 5-10MB por dominio)
- Solo almacena strings (objetos y arrays deben ser convertidos con JSON)
- API síncrona (puede bloquear el hilo principal)
- No es adecuado para grandes cantidades de datos
- Sin soporte para búsquedas complejas
Cookies
Las cookies son pequeños fragmentos de datos enviados desde un sitio web y almacenados en el navegador del usuario.
Creación y gestión de cookies
// Crear una cookie básica
document.cookie = "usuario=Juan";
// Crear una cookie con opciones adicionales
document.cookie = "preferencia=oscuro; expires=Fri, 31 Dec 2023 23:59:59 GMT; path=/; secure; samesite=strict";
// Leer todas las cookies
const todasLasCookies = document.cookie;
console.log(todasLasCookies); // "usuario=Juan; preferencia=oscuro"
// Función para crear cookies con facilidad
function setCookie(nombre, valor, diasExpiracion, path = '/') {
const d = new Date();
d.setTime(d.getTime() + (diasExpiracion * 24 * 60 * 60 * 1000));
const expires = `expires=${d.toUTCString()}`;
document.cookie = `${nombre}=${valor}; ${expires}; path=${path}; secure; samesite=strict`;
}
// Función para obtener el valor de una cookie específica
function getCookie(nombre) {
const cookieArr = document.cookie.split(';');
for(let i = 0; i < cookieArr.length; i++) {
const cookiePair = cookieArr[i].split('=');
const cookieName = cookiePair[0].trim();
if(cookieName === nombre) {
return decodeURIComponent(cookiePair[1]);
}
}
return null;
}
// Función para eliminar una cookie
function deleteCookie(nombre) {
document.cookie = `${nombre}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
}
Opciones de cookies
- expires/max-age: Define cuando expira la cookie
- path: Especifica la ruta donde la cookie es válida
- domain: Especifica el dominio donde la cookie es válida
- secure: La cookie solo se envía en conexiones HTTPS
- samesite: Controla si la cookie se envía con solicitudes entre sitios
- httpOnly: Evita acceso mediante JavaScript (solo configurable desde el servidor)
Limitaciones de cookies
- Tamaño máximo de 4KB por cookie
- Número limitado de cookies por dominio (generalmente 50-60)
- Se envían en cada solicitud HTTP al servidor (overhead)
- Problemas de privacidad y regulaciones (GDPR, ePrivacy, etc.)
IndexedDB
IndexedDB es una base de datos NoSQL orientada a objetos que permite almacenar grandes cantidades de datos estructurados, incluyendo archivos y blobs.
Características principales
- API asíncrona (no bloquea el hilo principal)
- Transaccional
- Basada en objetos (no solo strings)
- Soporta índices para búsquedas rápidas
- Mayor capacidad de almacenamiento
Uso básico de IndexedDB
// Abrir o crear una base de datos
const request = indexedDB.open('miBaseDeDatos', 1);
// Manejar la creación/actualización de la estructura
request.onupgradeneeded = function(event) {
const db = event.target.result;
// Crear un almacén de objetos (similar a una tabla)
const usuariosStore = db.createObjectStore('usuarios', { keyPath: 'id' });
// Crear índices para búsquedas rápidas
usuariosStore.createIndex('por_nombre', 'nombre', { unique: false });
usuariosStore.createIndex('por_email', 'email', { unique: true });
};
// Manejar errores
request.onerror = function(event) {
console.error('Error al abrir la base de datos:', event.target.error);
};
// Éxito al abrir la base de datos
request.onsuccess = function(event) {
const db = event.target.result;
// Agregar datos
function agregarUsuario(usuario) {
const transaction = db.transaction(['usuarios'], 'readwrite');
const store = transaction.objectStore('usuarios');
const request = store.add(usuario);
request.onsuccess = function() {
console.log('Usuario agregado con ID:', request.result);
};
request.onerror = function() {
console.error('Error al agregar usuario:', request.error);
};
}
// Leer datos
function obtenerUsuario(id) {
const transaction = db.transaction(['usuarios']);
const store = transaction.objectStore('usuarios');
const request = store.get(id);
request.onsuccess = function() {
if (request.result) {
console.log('Usuario encontrado:', request.result);
} else {
console.log('Usuario no encontrado');
}
};
}
// Actualizar datos
function actualizarUsuario(usuario) {
const transaction = db.transaction(['usuarios'], 'readwrite');
const store = transaction.objectStore('usuarios');
const request = store.put(usuario);
request.onsuccess = function() {
console.log('Usuario actualizado');
};
}
// Eliminar datos
function eliminarUsuario(id) {
const transaction = db.transaction(['usuarios'], 'readwrite');
const store = transaction.objectStore('usuarios');
const request = store.delete(id);
request.onsuccess = function() {
console.log('Usuario eliminado');
};
}
// Ejemplo de uso
agregarUsuario({ id: 1, nombre: 'Juan Pérez', email: 'juan@ejemplo.com', edad: 30 });
obtenerUsuario(1);
};
Cache API
La Cache API es parte de los Service Workers y permite almacenar respuestas HTTP para su uso offline.
// Abrir o crear una caché
caches.open('v1').then(cache => {
// Agregar recursos a la caché
cache.add('/index.html');
// Agregar múltiples recursos
cache.addAll([
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png'
]);
// Agregar una respuesta personalizada
cache.put('/api/data', new Response(JSON.stringify({ data: 'valor' }), {
headers: { 'Content-Type': 'application/json' }
}));
});
// Buscar en la caché
caches.match('/index.html').then(response => {
if (response) {
console.log('Recurso encontrado en caché');
return response;
} else {
console.log('Recurso no encontrado en caché');
return fetch('/index.html');
}
});
// Eliminar recursos de la caché
caches.open('v1').then(cache => {
cache.delete('/index.html');
});
// Eliminar una caché completa
caches.delete('v1');
Comparación de mecanismos de almacenamiento
Mecanismo | Capacidad | Persistencia | API | Uso ideal |
---|---|---|---|---|
LocalStorage | 5-10MB | Permanente | Síncrona | Preferencias, configuraciones |
SessionStorage | 5-10MB | Sesión | Síncrona | Datos temporales de sesión |
Cookies | 4KB | Configurable | Síncrona | Autenticación, tracking |
IndexedDB | >50MB | Permanente | Asíncrona | Grandes conjuntos de datos estructurados |
Cache API | Según dispositivo | Permanente | Asíncrona | Recursos HTTP para uso offline |
Seguridad en el almacenamiento del lado del cliente
Mejores prácticas de seguridad
- No almacenar datos sensibles
- Evitar contraseñas, tokens de larga duración, información personal sensible
- Validar datos almacenados antes de usarlos
try { const datos = JSON.parse(localStorage.getItem('datos')); // Validar datos antes de usarlos } catch (e) { console.error('Datos inválidos en localStorage'); localStorage.removeItem('datos'); }
- Usar flags de seguridad en cookies
Secure
: Solo HTTPSHttpOnly
: No accesible por JavaScript (solo desde servidor)SameSite
: Controla envío en peticiones cross-site
- Sanitizar datos antes de almacenarlos
// Sanitizar datos antes de almacenar function sanitizarDatos(datos) { // Implementar lógica de sanitización return datosLimpios; }
- Cifrar datos sensibles si es necesario almacenarlos
// Usando SubtleCrypto API (Web Crypto) async function cifrarDatos(datos, clave) { const encoded = new TextEncoder().encode(datos); const buffer = await crypto.subtle.encrypt( { name: 'AES-GCM', iv: window.crypto.getRandomValues(new Uint8Array(12)) }, clave, encoded ); return buffer; }
Sincronización con servidores
Estrategias de sincronización
- Sincronización bajo demanda
async function sincronizarDatos() { const datosLocales = JSON.parse(localStorage.getItem('datos')); try { const respuesta = await fetch('/api/sincronizar', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(datosLocales) }); const datosActualizados = await respuesta.json(); localStorage.setItem('datos', JSON.stringify(datosActualizados)); localStorage.setItem('ultimaSincronizacion', Date.now()); return datosActualizados; } catch (error) { console.error('Error al sincronizar:', error); return null; } }
- Sincronización periódica
function configurarSincronizacionPeriodica(intervaloMinutos) { // Sincronizar inmediatamente sincronizarDatos(); // Configurar sincronización periódica setInterval(sincronizarDatos, intervaloMinutos * 60 * 1000); }
- Sincronización en segundo plano con Service Workers
// En el Service Worker self.addEventListener('sync', event => { if (event.tag === 'sincronizar-datos') { event.waitUntil(sincronizarDatosEnSegundoPlano()); } }); // En la aplicación principal navigator.serviceWorker.ready.then(registration => { registration.sync.register('sincronizar-datos'); });
Patrones de uso comunes
Almacenamiento en caché de datos de API
async function obtenerDatos(url) {
// Verificar si hay datos en caché y si son recientes
const cachedData = localStorage.getItem(url);
const cachedTime = localStorage.getItem(`${url}_timestamp`);
const ahora = Date.now();
const expiracion = 60 * 60 * 1000; // 1 hora en milisegundos
// Si hay datos en caché y no han expirado
if (cachedData && cachedTime && (ahora - cachedTime < expiracion)) {
return JSON.parse(cachedData);
}
// Si no hay datos en caché o han expirado, hacer petición
try {
const response = await fetch(url);
const data = await response.json();
// Guardar en caché
localStorage.setItem(url, JSON.stringify(data));
localStorage.setItem(`${url}_timestamp`, ahora);
return data;
} catch (error) {
// Si hay error pero tenemos datos en caché, usarlos aunque estén expirados
if (cachedData) {
console.warn('Usando datos en caché expirados debido a error de red');
return JSON.parse(cachedData);
}
throw error;
}
}
Persistencia de estado de aplicación
// Gestor de estado simple
class EstadoApp {
constructor(clave = 'app_estado') {
this.clave = clave;
this.estado = this.cargar();
}
cargar() {
try {
const estadoGuardado = localStorage.getItem(this.clave);
return estadoGuardado ? JSON.parse(estadoGuardado) : {};
} catch (e) {
console.error('Error al cargar estado:', e);
return {};
}
}
guardar() {
try {
localStorage.setItem(this.clave, JSON.stringify(this.estado));
} catch (e) {
console.error('Error al guardar estado:', e);
}
}
actualizar(cambios) {
this.estado = { ...this.estado, ...cambios };
this.guardar();
return this.estado;
}
obtener(clave) {
return clave ? this.estado[clave] : this.estado;
}
limpiar() {
this.estado = {};
localStorage.removeItem(this.clave);
}
}
// Uso
const appEstado = new EstadoApp();
appEstado.actualizar({ tema: 'oscuro', idioma: 'es' });
console.log(appEstado.obtener('tema')); // 'oscuro'