// 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();
✅ Ideal para:
❌ Evitar para:
// 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();
✅ Ideal para:
❌ Evitar para:
| 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 |
// 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";
// 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
// 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
✅ Ideal para:
❌ Evitar para:
// 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);
};
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
});
// 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);
};
}
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");
}
};
}
✅ Ideal para:
❌ Evitar para:
// 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);
})
);
});
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
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
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
// 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);
}
})
);
})
);
});
✅ Ideal para:
❌ Evitar para:
| 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 |
| 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 |
| 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 |
| 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 |
¿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
¿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
localStorage
sessionStorage
Cookies
IndexedDB
Cache API
1. Cross-Site Scripting (XSS)
2. Cross-Site Request Forgery (CSRF)
3. Man-in-the-Middle (MITM)
✅ 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
// ❌ 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');
}
}
// ❌ 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";
// ✅ 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
}
};
// ✅ 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
});
// 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', ... }
// 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);
// 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
// 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();
// 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))
);
})
);
});
// 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');
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: { ... } });
Operaciones Lectura (por operación)
Operaciones Escritura (por operación)
Conclusión: localStorage es más rápido para operaciones pequeñas, pero bloquea la UI. IndexedDB es mejor para grandes volúmenes.
// ❌ 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));
// ✅ 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);
});
}
// ✅ 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));
}
}
// 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
}
});
}
Inspeccionar almacenamiento:
Funcionalidades:
Ver cuota de almacenamiento:
chrome://quota-internals
Muestra:
Similar a Chrome:
Extra: Mejores herramientas para IndexedDB (visualización de estructura)
Chrome DevTools:
Útil:
chrome://serviceworker-internals
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:
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();
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:
MDN Web Docs: https://developer.mozilla.org/
Web.dev: https://web.dev/
Can I Use: https://caniuse.com/
Abstracción de Storage:
Service Workers:
Gestión de Cookies:
✅ 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
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)
Repositorio con ejemplos:
Referencias útiles:
Recuerda:
Slides disponibles en: [URL de tu repositorio]