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

  1. localStorage
  2. sessionStorage
  3. Cookies
  4. IndexedDB
  5. 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
sessionStorage Solo misma pestaña No
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:

  1. Abrir DevTools (F12)
  2. Ir a pestaña "Application"
  3. 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:

  1. DevTools → Storage
  2. Inspeccionar cada tipo de almacenamiento
  3. Editar/eliminar datos
  4. Buscar y filtrar

Extra: Mejores herramientas para IndexedDB (visualización de estructura)

Debugging Service Workers

Chrome DevTools:

  1. Application → Service Workers
  2. Ver estado (installing, waiting, active)
  3. Update, Unregister, Skip waiting
  4. 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

  1. Más privacidad: Restricciones en cookies third-party
  2. PWAs: Mayor adopción de Service Workers y Cache API
  3. Offline-first: Apps que funcionan sin conexión
  4. Mejor rendimiento: APIs asíncronas como prioridad
  5. Más capacidad: Navegadores aumentando límites

Recursos y Referencias

Documentación Oficial

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

Resumen y Conclusiones

Puntos Clave

  1. No hay una solución única: Cada mecanismo tiene su propósito
  2. Seguridad primero: NUNCA almacenes datos sensibles sin protección
  3. Usa HTTPS: Imprescindible en producción
  4. localStorage ≠ Autenticación: Usa cookies HttpOnly para tokens
  5. IndexedDB para datos grandes: Cuando necesites > 10 MB
  6. 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:

¡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