Almacenamiento en el Navegador
Persistencia de Datos en la Web
Agenda
- ¿Por qué necesitamos almacenamiento en el navegador?
- Mecanismos de almacenamiento disponibles
- Comparación detallada
- Cuándo usar cada uno
- Seguridad y mejores prácticas
- Ejemplos prácticos
¿Por qué almacenar datos en el navegador?
- Mejor experiencia de usuario: Mantener preferencias, estado de sesión
- Rendimiento: Reducir peticiones al servidor
- Funcionalidad offline: PWAs y aplicaciones sin conexión
- Personalización: Guardar configuraciones específicas del usuario
- Reducir latencia: Acceso inmediato a datos frecuentes
Mecanismos de Almacenamiento
- localStorage
- sessionStorage
- Cookies
- IndexedDB
- Cache API (Service Workers)
localStorage
localStorage: Características
- Capacidad: 5-10 MB por origen
- Persistencia: Permanente (hasta limpieza manual/caché)
- Alcance: Todas las pestañas/ventanas del mismo origen
- API: Síncrona y simple
- Tipo de datos: Solo strings (usar JSON.stringify/parse)
localStorage: Sintaxis
// Guardar datos
localStorage.setItem('tema', 'oscuro');
localStorage.setItem('usuario', JSON.stringify({
nombre: 'Ana',
idioma: 'es'
}));
// Leer datos
const tema = localStorage.getItem('tema');
const usuario = JSON.parse(localStorage.getItem('usuario'));
// Eliminar
localStorage.removeItem('tema');
// Limpiar todo
localStorage.clear();
localStorage: Casos de Uso
✅ Ideal para:
- Preferencias de usuario (tema, idioma)
- Configuraciones de UI
- Datos de caché no sensibles
- Flags de features
- Estado de onboarding
❌ Evitar para:
- Tokens de autenticación (disclaimer ???)
- Información personal sensible
- Datos que requieren expiración automática
localStorage: Ventajas
- ✅ API muy simple e intuitiva
- ✅ Persistencia a largo plazo
- ✅ Compatible con todos los navegadores modernos
- ✅ Sincronizado entre pestañas del mismo origen
- ✅ No se envía al servidor automáticamente
localStorage: Desventajas
- ❌ Vulnerable a ataques XSS
- ❌ Solo almacena strings (requiere serialización)
- ❌ API síncrona (puede bloquear UI con grandes volúmenes)
- ❌ No se puede compartir entre subdominios
- ❌ Sin política de expiración automática
- ❌ Capacidad limitada (5-10 MB)
sessionStorage
sessionStorage: Características
- Capacidad: 5-10 MB por origen
- Persistencia: Solo durante la sesión de la pestaña
- Alcance: Solo la pestaña actual
- API: Idéntica a localStorage
- Aislamiento: Cada pestaña tiene su propio almacenamiento
sessionStorage: Sintaxis
// API idéntica a localStorage
sessionStorage.setItem('formData', JSON.stringify({
paso: 2,
email: 'usuario@example.com'
}));
const formData = JSON.parse(sessionStorage.getItem('formData'));
sessionStorage.removeItem('formData');
sessionStorage.clear();
sessionStorage: Casos de Uso
✅ Ideal para:
- Formularios multi-paso
- Carrito de compras temporal
- Estado de navegación dentro de una sesión
- Tokens temporales de sesión
- Datos que NO deben persistir entre pestañas
❌ Evitar para:
- Datos que necesiten persistir al cerrar pestaña
- Información compartida entre ventanas
- Preferencias a largo plazo
sessionStorage: Ventajas
- ✅ API simple (igual que localStorage)
- ✅ Aislamiento entre pestañas (mayor seguridad)
- ✅ Se limpia automáticamente al cerrar pestaña
- ✅ Ideal para sesiones temporales
sessionStorage: Desventajas
- ❌ Vulnerable a XSS (como localStorage)
- ❌ No persiste al cerrar pestaña
- ❌ No se comparte entre pestañas
- ❌ API síncrona (bloquea UI)
- ❌ Solo strings
localStorage vs sessionStorage
| Aspecto | localStorage | sessionStorage |
|---|---|---|
| Persistencia | Permanente | Solo sesión |
| Alcance | Todas las pestañas | Una pestaña |
| Cuándo se elimina | Manual | Al cerrar pestaña |
| Compartir datos | Entre pestañas | No |
| Uso típico | Preferencias | Estado temporal |
Cookies
Cookies: Características
- Capacidad: ~4 KB por cookie
- Persistencia: Configurable (expires/max-age)
- Alcance: Configurable (domain, path)
- Enviadas automáticamente: Sí, en cada petición HTTP
- Acceso: Cliente (JavaScript) y servidor
Cookies: Sintaxis JavaScript
// Crear cookie simple
document.cookie = "usuario=Ana";
// Con expiración
document.cookie = "tema=oscuro; max-age=31536000"; // 1 año
// Con path y domain
document.cookie = "idioma=es; path=/; domain=.example.com";
// Leer cookies
const cookies = document.cookie.split('; ');
const tema = cookies.find(c => c.startsWith('tema='))?.split('=')[1];
// Eliminar (expirar)
document.cookie = "tema=; max-age=0";
Cookies: Atributos de Seguridad
// Configuración segura completa
document.cookie = "sessionId=abc123; " +
"Secure; " + // Solo HTTPS
"HttpOnly; " + // No accesible desde JavaScript
"SameSite=Strict; " + // Protección CSRF
"max-age=3600; " + // Expira en 1 hora
"path=/"; // Disponible en todo el sitio
Nota: HttpOnly solo se puede configurar desde el servidor
Cookies: Atributo SameSite
// Strict: Cookie solo se envía en requests del mismo sitio
Set-Cookie: sessionId=xyz; SameSite=Strict
// Lax: Cookie se envía en navegación top-level (default)
Set-Cookie: tracking=123; SameSite=Lax
// None: Cookie se envía en todos los requests (requiere Secure)
Set-Cookie: widget=abc; SameSite=None; Secure
Cookies: Casos de Uso
✅ Ideal para:
- Autenticación: Session IDs (con HttpOnly + Secure)
- Tokens CSRF
- Preferencias que necesita el servidor
- Analytics y tracking
- Single Sign-On (SSO)
❌ Evitar para:
- Grandes volúmenes de datos
- Datos que NO necesita el servidor
- Información que cambia frecuentemente
Cookies: Ventajas
- ✅ Configuración de expiración automática
- ✅ Se envían automáticamente con requests HTTP
- ✅ Atributos de seguridad (HttpOnly, Secure, SameSite)
- ✅ Control granular (domain, path)
- ✅ Soporte universal (todos los navegadores)
- ✅ Accesibles desde servidor y cliente
Cookies: Desventajas
- ❌ Capacidad muy limitada (~4 KB)
- ❌ Se envían en TODAS las peticiones (overhead)
- ❌ API compleja de manejar en JavaScript
- ❌ Vulnerable a CSRF (sin SameSite)
- ❌ Vulnerable a XSS (sin HttpOnly)
- ❌ Pueden ser bloqueadas por usuarios/navegadores
IndexedDB
IndexedDB: Características
- Capacidad: Cientos de MB a GB (60% del disco)
- Persistencia: Permanente
- API: Asíncrona (basada en eventos/promesas)
- Tipo de datos: Objetos JavaScript, arrays, blobs, archivos
- Estructura: Base de datos NoSQL con índices
IndexedDB: Conceptos Clave
- Database: Contenedor principal
- Object Store: Similar a una tabla
- Index: Para búsquedas eficientes
- Transaction: Operaciones ACID
- Cursor: Para iterar sobre registros
- Key Path: Identificador único de registros
IndexedDB: Abrir Base de Datos
// Abrir (o crear) base de datos
const request = indexedDB.open("MiApp", 1);
// Crear schema (solo en upgradeneeded)
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Crear object store
const store = db.createObjectStore("usuarios", {
keyPath: "id",
autoIncrement: true
});
// Crear índices
store.createIndex("email", "email", { unique: true });
store.createIndex("nombre", "nombre");
};
request.onsuccess = (event) => {
const db = event.target.result;
console.log("DB abierta:", db);
};
IndexedDB: Insertar Datos
function agregarUsuario(db, usuario) {
const tx = db.transaction("usuarios", "readwrite");
const store = tx.objectStore("usuarios");
const request = store.add(usuario);
request.onsuccess = () => {
console.log("Usuario agregado:", request.result);
};
request.onerror = () => {
console.error("Error:", request.error);
};
tx.oncomplete = () => {
console.log("Transacción completada");
};
}
// Uso
agregarUsuario(db, {
nombre: "Ana García",
email: "ana@example.com",
edad: 28
});
IndexedDB: Leer Datos
// Leer por clave primaria
function obtenerUsuario(db, id) {
const tx = db.transaction("usuarios", "readonly");
const store = tx.objectStore("usuarios");
const request = store.get(id);
request.onsuccess = () => {
console.log("Usuario:", request.result);
};
}
// Leer por índice
function buscarPorEmail(db, email) {
const tx = db.transaction("usuarios", "readonly");
const store = tx.objectStore("usuarios");
const index = store.index("email");
const request = index.get(email);
request.onsuccess = () => {
console.log("Usuario:", request.result);
};
}
IndexedDB: Consultas con Cursores
function listarUsuarios(db) {
const tx = db.transaction("usuarios", "readonly");
const store = tx.objectStore("usuarios");
const request = store.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
console.log("Usuario:", cursor.value);
cursor.continue(); // Siguiente registro
} else {
console.log("Fin de registros");
}
};
}
IndexedDB: Casos de Uso
✅ Ideal para:
- Progressive Web Apps (PWA)
- Almacenamiento offline de datos complejos
- Caché de grandes conjuntos de datos
- Aplicaciones que manejan archivos/blobs
- Sistemas que requieren consultas complejas
- Apps con sincronización de datos
❌ Evitar para:
- Datos simples clave-valor
- Necesidades de almacenamiento pequeño
- Cuando se requiere API síncrona
IndexedDB: Ventajas
- ✅ Gran capacidad de almacenamiento (GB)
- ✅ API asíncrona (no bloquea UI)
- ✅ Soporta tipos de datos complejos
- ✅ Transacciones ACID
- ✅ Índices para búsquedas eficientes
- ✅ Ideal para aplicaciones offline
IndexedDB: Desventajas
- ❌ API compleja y verbosa
- ❌ Curva de aprendizaje pronunciada
- ❌ Manejo de errores más complejo
- ❌ Vulnerable a XSS
- ❌ Requiere gestión manual de versiones
Cache API
Cache API: Características
- Capacidad: Similar a IndexedDB (GB)
- Contexto: Service Workers (también window en algunos navegadores)
- Propósito: Almacenar recursos HTTP (Request/Response)
- API: Asíncrona (basada en promesas)
- Uso principal: Progressive Web Apps (PWA)
Cache API: Sintaxis Básica
// En un Service Worker
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('mi-cache-v1').then((cache) => {
return cache.addAll([
'/',
'/styles.css',
'/script.js',
'/images/logo.png'
]);
})
);
});
// Responder desde caché
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});
Cache API: Estrategias de Caché
1. Cache First (Cache, falling back to network)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});
Ideal para: Assets estáticos, imágenes, CSS, JS
Cache API: Estrategias (cont.)
2. Network First (Network, falling back to cache)
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request).catch(() => {
return caches.match(event.request);
})
);
});
Ideal para: Contenido dinámico, datos que cambian frecuentemente
Cache API: Estrategias (cont.)
3. Stale While Revalidate
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open('mi-cache').then((cache) => {
return cache.match(event.request).then((response) => {
const fetchPromise = fetch(event.request).then((networkResponse) => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
return response || fetchPromise;
});
})
);
});
Ideal para: Balance entre velocidad y actualización
Cache API: Gestión de Caché
// Listar cachés
caches.keys().then((cacheNames) => {
console.log('Cachés:', cacheNames);
});
// Eliminar caché antiguo
caches.delete('mi-cache-v1');
// Limpiar cachés viejos
self.addEventListener('activate', (event) => {
const cacheWhitelist = ['mi-cache-v2'];
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (!cacheWhitelist.includes(cacheName)) {
return caches.delete(cacheName);
}
})
);
})
);
});
Cache API: Casos de Uso
✅ Ideal para:
- Progressive Web Apps (PWA)
- Funcionalidad offline completa
- Caché de recursos estáticos
- Caché de respuestas API
- Mejora de rendimiento
- Reducción de uso de red
❌ Evitar para:
- Almacenamiento de datos estructurados
- Aplicaciones sin Service Workers
- Datos que requieren consultas complejas
Cache API: Ventajas
- ✅ Gran capacidad
- ✅ API asíncrona moderna (Promises)
- ✅ Control total sobre estrategias de caché
- ✅ Perfecto para PWAs
- ✅ Funcionalidad offline
- ✅ Almacena requests/responses completos
Cache API: Desventajas
- ❌ Requiere Service Workers
- ❌ Configuración más compleja
- ❌ Curva de aprendizaje
- ❌ No todos los navegadores (modernos sí)
- ❌ Debugging puede ser complicado
Comparación Completa
Tabla Comparativa: Capacidad y Persistencia
| Mecanismo | Capacidad | Persistencia | Expira |
|---|---|---|---|
| localStorage | 5-10 MB | Permanente | Manual |
| sessionStorage | 5-10 MB | Por sesión | Al cerrar pestaña |
| Cookies | ~4 KB | Configurable | Configurable |
| IndexedDB | ~60% disco | Permanente | Manual |
| Cache API | ~60% disco | Permanente | Manual |
Tabla Comparativa: Alcance y Acceso
| Mecanismo | Accesible desde | Enviado con requests | Bloquea UI |
|---|---|---|---|
| localStorage | Cualquier pestaña (mismo origen) | No | Sí |
| sessionStorage | Solo misma pestaña | No | Sí |
| Cookies | Cualquier pestaña | Sí (automático) | No |
| IndexedDB | Cualquier pestaña | No | No |
| Cache API | Service Worker | No | No |
Tabla Comparativa: Tipos de Datos y API
| Mecanismo | Tipo de datos | API | Complejidad |
|---|---|---|---|
| localStorage | Solo strings | Síncrona | Baja |
| sessionStorage | Solo strings | Síncrona | Baja |
| Cookies | Solo strings | Síncrona | Media |
| IndexedDB | Objetos, arrays, blobs | Asíncrona | Alta |
| Cache API | Request/Response | Asíncrona | Media-Alta |
Tabla Comparativa: Seguridad
| Mecanismo | Vulnerable a XSS | Vulnerable a CSRF | Protección disponible |
|---|---|---|---|
| localStorage | ✅ Sí | ❌ No | Ninguna nativa |
| sessionStorage | ✅ Sí | ❌ No | Aislamiento por pestaña |
| Cookies | Solo si NO HttpOnly | Solo si NO SameSite | HttpOnly, Secure, SameSite |
| IndexedDB | ✅ Sí | ❌ No | Ninguna nativa |
| Cache API | Limitado (Service Worker) | ❌ No | Scope de Service Worker |
Cuándo Usar Cada Uno
Matriz de Decisión
¿Datos sensibles (tokens, passwords)?
├─ Sí → Cookies (HttpOnly + Secure + SameSite)
└─ No → ¿Cuánto espacio necesitas?
├─ < 4 KB → ¿El servidor lo necesita?
│ ├─ Sí → Cookies
│ └─ No → localStorage o sessionStorage
├─ 4 KB - 10 MB → localStorage o sessionStorage
└─ > 10 MB → IndexedDB o Cache API
Matriz de Decisión (cont.)
¿Necesita persistir entre sesiones?
├─ Sí → localStorage, Cookies (con expires), IndexedDB
└─ No → sessionStorage
¿Necesita funcionalidad offline?
├─ Sí → Cache API + IndexedDB (PWA)
└─ No → Cualquier otro según necesidad
¿Requiere consultas complejas?
├─ Sí → IndexedDB
└─ No → Otros mecanismos
Casos de Uso por Mecanismo
localStorage
- ✅ Preferencias de tema (oscuro/claro)
- ✅ Idioma de la aplicación
- ✅ Configuración de UI
- ✅ Flags de features
- ✅ Datos de onboarding completado
Casos de Uso por Mecanismo (cont.)
sessionStorage
- ✅ Formularios multi-paso
- ✅ Estado de wizard/asistente
- ✅ Carrito temporal de compras
- ✅ Datos de navegación temporal
- ✅ Estados de UI que NO deben persistir
Casos de Uso por Mecanismo (cont.)
Cookies
- ✅ Session IDs de autenticación
- ✅ Tokens CSRF
- ✅ Preferencias que necesita el servidor
- ✅ Analytics y tracking
- ✅ A/B testing flags
- ✅ SSO (Single Sign-On)
Casos de Uso por Mecanismo (cont.)
IndexedDB
- ✅ Progressive Web Apps
- ✅ Aplicaciones offline-first
- ✅ Caché de datos complejos
- ✅ Sincronización de datos
- ✅ Almacenamiento de archivos/blobs
- ✅ Aplicaciones que manejan grandes datasets
Casos de Uso por Mecanismo (cont.)
Cache API
- ✅ Progressive Web Apps
- ✅ Caché de recursos estáticos (CSS, JS, imágenes)
- ✅ Caché de respuestas de API
- ✅ Funcionalidad offline completa
- ✅ Estrategias de caché personalizadas
- ✅ Precarga de recursos
Seguridad y Mejores Prácticas
Amenazas Principales
1. Cross-Site Scripting (XSS)
- Código malicioso inyectado que puede leer localStorage/sessionStorage
- Puede robar cookies sin HttpOnly
- Puede acceder a IndexedDB
2. Cross-Site Request Forgery (CSRF)
- Uso no autorizado de cookies
- Mitigado con SameSite
3. Man-in-the-Middle (MITM)
- Interceptación de cookies sin Secure
- Requiere HTTPS
Mejores Prácticas: General
✅ NUNCA almacenes datos sensibles sin encriptar ✅ Valida y sanitiza todos los inputs del usuario ✅ Usa HTTPS siempre en producción ✅ Implementa CSP (Content Security Policy) ✅ Limpia datos antiguos regularmente ✅ Maneja errores de cuota excedida
Mejores Prácticas: localStorage/sessionStorage
// ❌ MAL - Almacenar token directamente
localStorage.setItem('authToken', token);
// ✅ BIEN - NO almacenar tokens sensibles aquí
// Usar cookies HttpOnly en su lugar
// ✅ BIEN - Manejar errores de cuota
try {
localStorage.setItem('preferencias', JSON.stringify(data));
} catch (e) {
if (e.name === 'QuotaExceededError') {
console.error('Cuota de almacenamiento excedida');
// Limpiar datos antiguos
}
}
// ✅ BIEN - Validar datos al leer
const raw = localStorage.getItem('config');
if (raw) {
try {
const config = JSON.parse(raw);
// Validar estructura
if (config && typeof config === 'object') {
// Usar config
}
} catch (e) {
console.error('Datos corruptos');
}
}
Mejores Prácticas: Cookies
// ❌ MAL - Cookie insegura
document.cookie = "sessionId=abc123";
// ✅ BIEN - Cookie con todas las protecciones (desde servidor)
Set-Cookie: sessionId=abc123;
HttpOnly; // No accesible desde JS
Secure; // Solo HTTPS
SameSite=Strict; // Protección CSRF
Max-Age=3600; // Expira en 1 hora
Path=/; // Scope
// ✅ BIEN - Cookie de preferencia (no sensible)
document.cookie = "tema=oscuro; SameSite=Lax; Secure; Max-Age=31536000";
Mejores Prácticas: IndexedDB
// ✅ BIEN - Manejo de errores completo
function guardarDatos(db, data) {
return new Promise((resolve, reject) => {
const tx = db.transaction('store', 'readwrite');
tx.onerror = () => reject(tx.error);
tx.oncomplete = () => resolve();
const store = tx.objectStore('store');
const request = store.add(data);
request.onerror = () => reject(request.error);
});
}
// ✅ BIEN - Versionado adecuado
const request = indexedDB.open('MiDB', 2); // Incrementar versión
request.onupgradeneeded = (event) => {
const db = event.target.result;
const oldVersion = event.oldVersion;
if (oldVersion < 1) {
// Crear stores iniciales
}
if (oldVersion < 2) {
// Migraciones de versión 2
}
};
Mejores Prácticas: Cache API
// ✅ BIEN - Gestión de versiones de caché
const CACHE_VERSION = 'v2';
const CACHE_NAME = `mi-app-${CACHE_VERSION}`;
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
);
})
);
});
// ✅ BIEN - No cachear datos sensibles
self.addEventListener('fetch', (event) => {
// No cachear requests con auth headers
if (event.request.headers.get('Authorization')) {
return fetch(event.request);
}
// Estrategia de caché para otros requests
});
Checklist de Seguridad
- ¿Estoy usando HTTPS en producción?
- ¿He configurado HttpOnly en cookies con datos sensibles?
- ¿He configurado Secure en todas las cookies?
- ¿He configurado SameSite en las cookies?
- ¿Estoy validando todos los inputs del usuario?
- ¿Tengo CSP configurado?
- ¿Estoy usando tokens sensibles solo en cookies HttpOnly?
- ¿Estoy limpiando datos antiguos/innecesarios?
- ¿Manejo errores de cuota excedida?
Ejemplos Prácticos
Ejemplo 1: Sistema de Preferencias
// Módulo de preferencias con localStorage
class PreferenciasUsuario {
constructor() {
this.KEY = 'preferencias_usuario';
this.defaults = {
tema: 'claro',
idioma: 'es',
notificaciones: true
};
}
obtener() {
try {
const raw = localStorage.getItem(this.KEY);
return raw ? { ...this.defaults, ...JSON.parse(raw) } : this.defaults;
} catch (e) {
console.error('Error al leer preferencias:', e);
return this.defaults;
}
}
guardar(preferencias) {
try {
localStorage.setItem(this.KEY, JSON.stringify(preferencias));
return true;
} catch (e) {
console.error('Error al guardar preferencias:', e);
return false;
}
}
actualizar(cambios) {
const actual = this.obtener();
return this.guardar({ ...actual, ...cambios });
}
}
// Uso
const prefs = new PreferenciasUsuario();
prefs.actualizar({ tema: 'oscuro' });
console.log(prefs.obtener()); // { tema: 'oscuro', idioma: 'es', ... }
Ejemplo 2: Formulario Multi-Paso
// Guardar progreso del formulario con sessionStorage
class FormularioMultiPaso {
constructor(formId) {
this.formId = formId;
this.KEY = `form_${formId}`;
}
guardarPaso(paso, datos) {
const progreso = this.obtenerProgreso();
progreso.pasoActual = paso;
progreso.datos[paso] = datos;
try {
sessionStorage.setItem(this.KEY, JSON.stringify(progreso));
} catch (e) {
console.error('Error al guardar progreso:', e);
}
}
obtenerProgreso() {
const raw = sessionStorage.getItem(this.KEY);
return raw ? JSON.parse(raw) : { pasoActual: 1, datos: {} };
}
limpiar() {
sessionStorage.removeItem(this.KEY);
}
}
// Uso
const form = new FormularioMultiPaso('registro');
form.guardarPaso(2, { nombre: 'Ana', email: 'ana@example.com' });
// Al recargar página
const progreso = form.obtenerProgreso();
console.log('Continuar en paso:', progreso.pasoActual);
Ejemplo 3: Sistema de Autenticación
// Backend (Node.js + Express)
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await autenticar(email, password);
if (user) {
// Generar session ID
const sessionId = generarSessionId();
await guardarSesion(sessionId, user.id);
// Configurar cookie segura
res.cookie('sessionId', sessionId, {
httpOnly: true, // No accesible desde JavaScript
secure: true, // Solo HTTPS
sameSite: 'strict', // Protección CSRF
maxAge: 3600000 // 1 hora
});
res.json({ success: true, user: { id: user.id, nombre: user.nombre } });
} else {
res.status(401).json({ success: false });
}
});
// Frontend - Guardar datos NO sensibles del usuario
localStorage.setItem('usuario_nombre', user.nombre);
localStorage.setItem('usuario_id', user.id);
// El token de sesión NUNCA se almacena en localStorage
// Solo en cookie HttpOnly
Ejemplo 4: App Offline con IndexedDB
// Gestor de datos offline
class GestorDatosOffline {
constructor() {
this.dbName = 'MiAppDB';
this.version = 1;
this.db = null;
}
async inicializar() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('articulos')) {
const store = db.createObjectStore('articulos', {
keyPath: 'id',
autoIncrement: true
});
store.createIndex('titulo', 'titulo', { unique: false });
store.createIndex('fecha', 'fecha', { unique: false });
}
};
});
}
async guardarArticulo(articulo) {
const tx = this.db.transaction('articulos', 'readwrite');
const store = tx.objectStore('articulos');
return new Promise((resolve, reject) => {
const request = store.add(articulo);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async obtenerTodos() {
const tx = this.db.transaction('articulos', 'readonly');
const store = tx.objectStore('articulos');
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
// Uso
const gestor = new GestorDatosOffline();
await gestor.inicializar();
await gestor.guardarArticulo({
titulo: 'Mi Artículo',
contenido: 'Lorem ipsum...',
fecha: new Date()
});
const articulos = await gestor.obtenerTodos();
Ejemplo 5: PWA con Cache API
// service-worker.js
const CACHE_NAME = 'mi-pwa-v1';
const urlsToCache = [
'/',
'/styles.css',
'/app.js',
'/logo.png'
];
// Instalación: Pre-cachear recursos
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(urlsToCache))
);
});
// Fetch: Estrategia Cache First para assets, Network First para API
self.addEventListener('fetch', (event) => {
const { request } = event;
// Network First para API
if (request.url.includes('/api/')) {
event.respondWith(
fetch(request)
.then((response) => {
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, responseClone);
});
return response;
})
.catch(() => caches.match(request))
);
return;
}
// Cache First para assets
event.respondWith(
caches.match(request)
.then((response) => response || fetch(request))
);
});
// Activación: Limpiar cachés antiguas
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
);
})
);
});
Ejemplo 6: Caché con Expiración
// Wrapper para localStorage con expiración
class CacheConExpiracion {
setItem(key, value, ttlMinutos) {
const item = {
value,
expira: Date.now() + (ttlMinutos * 60 * 1000)
};
try {
localStorage.setItem(key, JSON.stringify(item));
return true;
} catch (e) {
console.error('Error al guardar:', e);
return false;
}
}
getItem(key) {
const raw = localStorage.getItem(key);
if (!raw) return null;
try {
const item = JSON.parse(raw);
// Verificar expiración
if (Date.now() > item.expira) {
localStorage.removeItem(key);
return null;
}
return item.value;
} catch (e) {
console.error('Error al leer:', e);
return null;
}
}
removeItem(key) {
localStorage.removeItem(key);
}
}
// Uso
const cache = new CacheConExpiracion();
// Guardar por 30 minutos
cache.setItem('datos_usuario', { nombre: 'Ana' }, 30);
// Leer (retorna null si expiró)
const datos = cache.getItem('datos_usuario');
Ejemplo 7: Sincronización Online/Offline
class SincronizadorDatos {
constructor() {
this.QUEUE_KEY = 'sync_queue';
this.inicializarListeners();
}
inicializarListeners() {
window.addEventListener('online', () => this.sincronizar());
window.addEventListener('offline', () => {
console.log('Modo offline activado');
});
}
async guardarAccion(accion) {
// Intentar enviar al servidor
if (navigator.onLine) {
try {
await this.enviarAlServidor(accion);
return true;
} catch (e) {
console.error('Error al enviar, guardando localmente');
}
}
// Guardar en cola local si offline o falla
this.agregarACola(accion);
return false;
}
agregarACola(accion) {
const cola = this.obtenerCola();
cola.push({
...accion,
timestamp: Date.now(),
id: crypto.randomUUID()
});
localStorage.setItem(this.QUEUE_KEY, JSON.stringify(cola));
}
obtenerCola() {
const raw = localStorage.getItem(this.QUEUE_KEY);
return raw ? JSON.parse(raw) : [];
}
async sincronizar() {
console.log('Sincronizando datos pendientes...');
const cola = this.obtenerCola();
for (const accion of cola) {
try {
await this.enviarAlServidor(accion);
// Remover de cola si éxito
this.removerDeCola(accion.id);
} catch (e) {
console.error('Error al sincronizar acción:', accion.id);
}
}
}
removerDeCola(id) {
const cola = this.obtenerCola().filter(a => a.id !== id);
localStorage.setItem(this.QUEUE_KEY, JSON.stringify(cola));
}
async enviarAlServidor(accion) {
const response = await fetch('/api/acciones', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(accion)
});
if (!response.ok) throw new Error('Error al enviar');
return response.json();
}
}
// Uso
const sync = new SincronizadorDatos();
await sync.guardarAccion({ tipo: 'crear', datos: { ... } });
Rendimiento y Optimización
Rendimiento: localStorage vs IndexedDB
Operaciones Lectura (por operación)
- localStorage: ~0.005 ms (muy rápido, pero síncrono)
- IndexedDB: ~0.6 ms (más lento, pero asíncrono)
Operaciones Escritura (por operación)
- localStorage: ~0.017 ms
- IndexedDB: ~3 ms
Conclusión: localStorage es más rápido para operaciones pequeñas, pero bloquea la UI. IndexedDB es mejor para grandes volúmenes.
Optimización: Minimizar Operaciones
// ❌ MAL - Múltiples operaciones
localStorage.setItem('pref_tema', 'oscuro');
localStorage.setItem('pref_idioma', 'es');
localStorage.setItem('pref_notif', 'true');
// ✅ BIEN - Una sola operación
const preferencias = {
tema: 'oscuro',
idioma: 'es',
notificaciones: true
};
localStorage.setItem('preferencias', JSON.stringify(preferencias));
Optimización: Batch Operations IndexedDB
// ✅ BIEN - Insertar múltiples registros en una transacción
async function insertarMultiples(db, registros) {
const tx = db.transaction('store', 'readwrite');
const store = tx.objectStore('store');
registros.forEach(registro => store.add(registro));
return new Promise((resolve, reject) => {
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
Optimización: Lazy Loading
// ✅ BIEN - Cargar datos solo cuando se necesitan
class GestorDatos {
constructor() {
this.cache = null;
}
async obtenerDatos() {
// Usar caché en memoria si existe
if (this.cache) return this.cache;
// Cargar desde storage solo la primera vez
const raw = localStorage.getItem('datos');
this.cache = raw ? JSON.parse(raw) : [];
return this.cache;
}
async guardarDatos(datos) {
this.cache = datos;
localStorage.setItem('datos', JSON.stringify(datos));
}
}
Monitoreo de Cuota
// Verificar espacio disponible (Storage API)
if ('storage' in navigator && 'estimate' in navigator.storage) {
navigator.storage.estimate().then(({ usage, quota }) => {
const porcentaje = (usage / quota * 100).toFixed(2);
console.log(`Usando ${usage} bytes de ${quota} (${porcentaje}%)`);
if (porcentaje > 80) {
console.warn('Espacio de almacenamiento casi lleno');
// Limpiar datos antiguos
}
});
}
Herramientas de Debugging
Chrome DevTools: Application Tab
Inspeccionar almacenamiento:
- Abrir DevTools (F12)
- Ir a pestaña "Application"
- Panel izquierdo:
- Local Storage
- Session Storage
- Cookies
- IndexedDB
- Cache Storage
Funcionalidades:
- Ver/editar/eliminar datos
- Filtrar por clave
- Copiar valores
- Limpiar todo el almacenamiento
Chrome DevTools: Cuotas
Ver cuota de almacenamiento:
chrome://quota-internals
Muestra:
- Espacio usado por cada origen
- Cuota disponible
- Breakdown por tipo de almacenamiento
- Información de eviction
Firefox DevTools: Storage Inspector
Similar a Chrome:
- DevTools → Storage
- Inspeccionar cada tipo de almacenamiento
- Editar/eliminar datos
- Buscar y filtrar
Extra: Mejores herramientas para IndexedDB (visualización de estructura)
Debugging Service Workers
Chrome DevTools:
- Application → Service Workers
- Ver estado (installing, waiting, active)
- Update, Unregister, Skip waiting
- Ver errores en Console
Útil:
chrome://serviceworker-internals
Tendencias y Futuro
Storage Buckets API
Nueva API experimental para organizar almacenamiento:
// Crear buckets separados
const importantBucket = await navigator.storageBuckets.open('important', {
persisted: true,
durability: 'strict'
});
const cacheBucket = await navigator.storageBuckets.open('cache', {
persisted: false,
durability: 'relaxed'
});
Ventajas:
- Mejor organización
- Políticas de persistencia por bucket
- Mejor control de eviction
File System Access API
Acceso al sistema de archivos local:
// Guardar archivo
const handle = await window.showSaveFilePicker();
const writable = await handle.createWritable();
await writable.write(data);
await writable.close();
// Abrir archivo
const [fileHandle] = await window.showOpenFilePicker();
const file = await fileHandle.getFile();
const contents = await file.text();
Origin Private File System (OPFS)
Sistema de archivos privado y rápido:
// Obtener raíz del file system privado
const root = await navigator.storage.getDirectory();
// Crear archivo
const fileHandle = await root.getFileHandle('archivo.txt', { create: true });
const writable = await fileHandle.createWritable();
await writable.write('Contenido');
await writable.close();
Ventajas:
- Mucho más rápido que IndexedDB para archivos
- No requiere permisos del usuario
- Privado para el origen
Tendencias Actuales
- Más privacidad: Restricciones en cookies third-party
- PWAs: Mayor adopción de Service Workers y Cache API
- Offline-first: Apps que funcionan sin conexión
- Mejor rendimiento: APIs asíncronas como prioridad
- Más capacidad: Navegadores aumentando límites
Recursos y Referencias
Documentación Oficial
-
MDN Web Docs: https://developer.mozilla.org/
- Web Storage API
- IndexedDB
- Service Workers
- Cache API
-
Web.dev: https://web.dev/
- Storage for the web
- Offline storage guide
-
Can I Use: https://caniuse.com/
- Verificar compatibilidad de navegadores
Herramientas y Librerías
Abstracción de Storage:
- localForage (IndexedDB con API simple)
- Dexie.js (Wrapper moderno para IndexedDB)
- idb (Promises para IndexedDB)
Service Workers:
- Workbox (Toolkit de Google para PWAs)
- sw-toolbox
Gestión de Cookies:
- js-cookie
- universal-cookie
Especificaciones W3C
- Storage Standard: https://storage.spec.whatwg.org/
- Web Storage: https://html.spec.whatwg.org/multipage/webstorage.html
- IndexedDB: https://w3c.github.io/IndexedDB/
- Service Workers: https://w3c.github.io/ServiceWorker/
- Cache API: https://w3c.github.io/ServiceWorker/#cache-interface
Resumen y Conclusiones
Puntos Clave
- No hay una solución única: Cada mecanismo tiene su propósito
- Seguridad primero: NUNCA almacenes datos sensibles sin protección
- Usa HTTPS: Imprescindible en producción
- localStorage ≠ Autenticación: Usa cookies HttpOnly para tokens
- IndexedDB para datos grandes: Cuando necesites > 10 MB
- Cache API para PWAs: Funcionalidad offline completa
Recomendaciones Finales
✅ Elige el mecanismo según tus necesidades específicas ✅ Implementa todas las medidas de seguridad disponibles ✅ Maneja errores de cuota excedida ✅ Limpia datos antiguos regularmente ✅ Prueba en diferentes navegadores ✅ Monitorea el uso de almacenamiento ✅ Documenta tus decisiones de arquitectura
Arquitectura Recomendada
Para una aplicación moderna:
Autenticación → Cookies (HttpOnly + Secure + SameSite)
Preferencias → localStorage
Estado temporal → sessionStorage
Datos complejos → IndexedDB
Caché de recursos → Cache API (Service Worker)
¿Preguntas?
Contacto y Recursos Adicionales
Repositorio con ejemplos:
- Todos los ejemplos de esta presentación
- Casos de uso completos
- Tests y demos
Referencias útiles:
- MDN Web Storage: https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API
- IndexedDB Guide: https://javascript.info/indexeddb
- Service Worker Cookbook: https://serviceworke.rs/
¡Gracias!
Recuerda:
- Seguridad primero
- Elige el mecanismo adecuado
- Prueba en diferentes navegadores
- Maneja errores apropiadamente
Slides disponibles en: [URL de tu repositorio]
Almacenamiento en el Navegador
By anlijudavid
Almacenamiento en el Navegador
- 27