HDP115

Almacenamiento en el Navegador

Aprende a almacenar datos en el navegador utilizando localStorage, sessionStorage y cookies para crear aplicaciones web con persistencia de datos.

CE

Cristian Escalante

Última actualización: 22 de abril de 2025

javascript
programación web
desarrollo frontend

Introducción al almacenamiento en el navegador

El almacenamiento en el navegador permite a las aplicaciones web guardar datos localmente en el dispositivo del usuario. Esto es útil para:

  • Mejorar el rendimiento al reducir peticiones al servidor
  • Guardar preferencias del usuario
  • Implementar funcionalidades offline
  • Mantener el estado de la aplicación entre sesiones
  • Almacenar datos temporales durante la navegación

Existen diferentes mecanismos de almacenamiento, cada uno con sus propias características, limitaciones y casos de uso.

Web Storage API

La Web Storage API proporciona dos mecanismos para almacenar datos de forma sencilla:

localStorage

Almacena datos sin fecha de expiración. Los datos persisten incluso después de cerrar el navegador.

// Guardar datos
localStorage.setItem('nombre', 'Ana');
localStorage.setItem('preferencias', JSON.stringify({
  tema: 'oscuro',
  notificaciones: true
}));

// Leer datos
const nombre = localStorage.getItem('nombre');
console.log(nombre); // "Ana"

const preferencias = JSON.parse(localStorage.getItem('preferencias'));
console.log(preferencias.tema); // "oscuro"

// Eliminar un elemento específico
localStorage.removeItem('nombre');

// Eliminar todos los datos
localStorage.clear();

sessionStorage

Similar a localStorage, pero los datos solo persisten durante la sesión actual del navegador. Cuando se cierra la pestaña o ventana, los datos se eliminan.

// Guardar datos
sessionStorage.setItem('carrito', JSON.stringify([
  { id: 1, nombre: 'Producto 1', cantidad: 2 },
  { id: 2, nombre: 'Producto 2', cantidad: 1 }
]));

// Leer datos
const carrito = JSON.parse(sessionStorage.getItem('carrito'));
console.log(carrito); // Array de productos

// Eliminar un elemento específico
sessionStorage.removeItem('carrito');

// Eliminar todos los datos
sessionStorage.clear();

Características comunes de Web Storage

  • Capacidad: Generalmente 5-10 MB por dominio
  • Almacenamiento: Solo cadenas de texto (strings)
  • Sincronía: Operaciones síncronas (pueden bloquear el hilo principal)
  • API simple: Métodos setItem(), getItem(), removeItem(), clear()
  • Eventos: Permite detectar cambios con el evento storage

Evento storage

El evento storage se dispara cuando los datos en localStorage cambian (en otras pestañas/ventanas):

window.addEventListener('storage', (event) => {
  console.log('Almacenamiento modificado:');
  console.log('Clave:', event.key);
  console.log('Valor anterior:', event.oldValue);
  console.log('Nuevo valor:', event.newValue);
  console.log('URL:', event.url);
  console.log('Área de almacenamiento:', event.storageArea);
});

Limitaciones de Web Storage

  • Solo almacena strings (necesitas usar JSON para objetos)
  • No es adecuado para grandes cantidades de datos
  • Operaciones síncronas pueden afectar el rendimiento
  • No tiene sistema de búsqueda o indexación
  • No soporta transacciones

Cookies

Las cookies son pequeños fragmentos de datos que el servidor envía al navegador del usuario. El navegador puede almacenar estos datos y enviarlos de vuelta al servidor en solicitudes posteriores.

Crear cookies con JavaScript

// Crear una cookie básica
document.cookie = "nombre=Juan";

// Crear una cookie con fecha de expiración
document.cookie = "usuario=ana123; expires=Fri, 31 Dec 2023 23:59:59 GMT";

// Crear una cookie con ruta específica
document.cookie = "idioma=es; path=/tienda";

// Crear una cookie segura (solo HTTPS)
document.cookie = "token=abc123; secure; samesite=strict";

// Crear una cookie HttpOnly (no accesible por JavaScript)
// Nota: Solo se puede establecer desde el servidor

Leer cookies

// Obtener todas las cookies
const todasLasCookies = document.cookie;
console.log(todasLasCookies); // "nombre=Juan; usuario=ana123; idioma=es"

// Función para obtener una cookie específica
function obtenerCookie(nombre) {
  const cookies = document.cookie.split('; ');
  const cookie = cookies.find(c => c.startsWith(nombre + '='));
  return cookie ? cookie.split('=')[1] : null;
}

const nombreUsuario = obtenerCookie('nombre');
console.log(nombreUsuario); // "Juan"

Modificar cookies

Para modificar una cookie, simplemente se crea una nueva con el mismo nombre:

document.cookie = "nombre=Carlos"; // Sobrescribe la cookie "nombre"

Eliminar cookies

Para eliminar una cookie, se establece una fecha de expiración en el pasado:

document.cookie = "nombre=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
document.cookie = "usuario=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";

Opciones de cookies

  • expires: Fecha de caducidad
  • max-age: Duración en segundos
  • path: Ruta en la que la cookie está disponible
  • domain: Dominio en el que la cookie está disponible
  • secure: La cookie solo se envía en conexiones HTTPS
  • samesite: Controla cuándo se envían las cookies en solicitudes entre sitios
  • httpOnly: La cookie no es accesible mediante JavaScript (solo desde el servidor)

Biblioteca para manejar cookies

Debido a la complejidad de manejar cookies manualmente, muchos desarrolladores utilizan bibliotecas:

// Ejemplo con js-cookie (https://github.com/js-cookie/js-cookie)
// npm install js-cookie

import Cookies from 'js-cookie';

// Establecer cookies
Cookies.set('nombre', 'Ana');
Cookies.set('preferencias', { tema: 'oscuro' }, { expires: 7 }); // 7 días

// Obtener cookies
const nombre = Cookies.get('nombre');
const preferencias = Cookies.get('preferencias');
console.log(JSON.parse(preferencias).tema); // "oscuro"

// Eliminar cookies
Cookies.remove('nombre');

Limitaciones de las cookies

  • Tamaño máximo: ~4KB por cookie
  • Número limitado por dominio (generalmente 50-300)
  • Se envían en cada petición HTTP (overhead)
  • Problemas de privacidad y regulaciones (GDPR, ePrivacy)
  • Complejidad en la gestión manual

IndexedDB

IndexedDB es una base de datos NoSQL orientada a objetos que permite almacenar grandes cantidades de datos estructurados, incluyendo archivos y blobs.

Características principales

  • Base de datos completa en el cliente
  • Almacenamiento de grandes volúmenes de datos
  • API asíncrona (no bloquea el hilo principal)
  • Soporta transacciones
  • Permite búsquedas indexadas
  • Estructura orientada a objetos

Abrir una base de datos

// Solicitar abrir una conexión a la base de datos
const request = indexedDB.open('miBaseDeDatos', 1);

// Manejar la creación/actualización de la estructura
request.onupgradeneeded = (event) => {
  const db = event.target.result;
  
  // Crear un almacén de objetos (similar a una tabla)
  const usuariosStore = db.createObjectStore('usuarios', { keyPath: 'id' });
  
  // Crear índices para búsquedas rápidas
  usuariosStore.createIndex('por_nombre', 'nombre', { unique: false });
  usuariosStore.createIndex('por_email', 'email', { unique: true });
};

// Manejar conexión exitosa
request.onsuccess = (event) => {
  const db = event.target.result;
  console.log('Base de datos abierta correctamente');
  
  // Usar la base de datos...
};

// Manejar errores
request.onerror = (event) => {
  console.error('Error al abrir la base de datos:', event.target.error);
};

Agregar datos

function agregarUsuario(db, usuario) {
  // Crear una transacción
  const transaction = db.transaction(['usuarios'], 'readwrite');
  
  // Obtener el almacén de objetos
  const usuariosStore = transaction.objectStore('usuarios');
  
  // Agregar el usuario
  const request = usuariosStore.add(usuario);
  
  // Manejar resultado
  request.onsuccess = () => {
    console.log('Usuario agregado correctamente');
  };
  
  request.onerror = (event) => {
    console.error('Error al agregar usuario:', event.target.error);
  };
}

// Uso
const nuevoUsuario = {
  id: 1,
  nombre: 'Ana García',
  email: 'ana@ejemplo.com',
  edad: 28
};

// Suponiendo que 'db' es la conexión a la base de datos
agregarUsuario(db, nuevoUsuario);

Leer datos

function obtenerUsuario(db, id) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['usuarios'], 'readonly');
    const usuariosStore = transaction.objectStore('usuarios');
    
    // Obtener por clave
    const request = usuariosStore.get(id);
    
    request.onsuccess = () => {
      if (request.result) {
        resolve(request.result);
      } else {
        reject(new Error('Usuario no encontrado'));
      }
    };
    
    request.onerror = (event) => {
      reject(event.target.error);
    };
  });
}

// Uso con async/await
async function mostrarUsuario(id) {
  try {
    const usuario = await obtenerUsuario(db, id);
    console.log('Usuario encontrado:', usuario);
  } catch (error) {
    console.error('Error:', error.message);
  }
}

Actualizar datos

function actualizarUsuario(db, usuario) {
  const transaction = db.transaction(['usuarios'], 'readwrite');
  const usuariosStore = transaction.objectStore('usuarios');
  
  // El método put actualiza si existe, o agrega si no existe
  const request = usuariosStore.put(usuario);
  
  request.onsuccess = () => {
    console.log('Usuario actualizado correctamente');
  };
  
  request.onerror = (event) => {
    console.error('Error al actualizar:', event.target.error);
  };
}

// Uso
const usuarioActualizado = {
  id: 1,
  nombre: 'Ana García Martínez',
  email: 'ana.garcia@ejemplo.com',
  edad: 29
};

actualizarUsuario(db, usuarioActualizado);

Eliminar datos

function eliminarUsuario(db, id) {
  const transaction = db.transaction(['usuarios'], 'readwrite');
  const usuariosStore = transaction.objectStore('usuarios');
  
  const request = usuariosStore.delete(id);
  
  request.onsuccess = () => {
    console.log('Usuario eliminado correctamente');
  };
  
  request.onerror = (event) => {
    console.error('Error al eliminar:', event.target.error);
  };
}

// Uso
eliminarUsuario(db, 1);

Consultas con índices

function buscarPorEmail(db, email) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['usuarios'], 'readonly');
    const usuariosStore = transaction.objectStore('usuarios');
    const emailIndex = usuariosStore.index('por_email');
    
    const request = emailIndex.get(email);
    
    request.onsuccess = () => {
      if (request.result) {
        resolve(request.result);
      } else {
        reject(new Error('Email no encontrado'));
      }
    };
    
    request.onerror = (event) => {
      reject(event.target.error);
    };
  });
}

// Uso
buscarPorEmail(db, 'ana@ejemplo.com')
  .then(usuario => console.log('Usuario encontrado:', usuario))
  .catch(error => console.error('Error:', error.message));

Recorrer todos los registros

function listarUsuarios(db) {
  return new Promise((resolve, reject) => {
    const usuarios = [];
    const transaction = db.transaction(['usuarios'], 'readonly');
    const usuariosStore = transaction.objectStore('usuarios');
    
    const request = usuariosStore.openCursor();
    
    request.onsuccess = (event) => {
      const cursor = event.target.result;
      
      if (cursor) {
        usuarios.push(cursor.value);
        cursor.continue(); // Avanzar al siguiente registro
      } else {
        // No hay más registros
        resolve(usuarios);
      }
    };
    
    request.onerror = (event) => {
      reject(event.target.error);
    };
  });
}

// Uso
listarUsuarios(db)
  .then(usuarios => {
    console.log('Total de usuarios:', usuarios.length);
    usuarios.forEach(usuario => console.log(usuario.nombre));
  })
  .catch(error => console.error('Error:', error));

Biblioteca para simplificar IndexedDB

Debido a la complejidad de la API nativa, existen bibliotecas que simplifican su uso:

// Ejemplo con Dexie.js (https://dexie.org/)
// npm install dexie

import Dexie from 'dexie';

// Definir la base de datos
const db = new Dexie('miBaseDeDatos');
db.version(1).stores({
  usuarios: '++id, nombre, email'
});

// Agregar datos
async function agregarUsuario() {
  try {
    const id = await db.usuarios.add({
      nombre: 'Ana García',
      email: 'ana@ejemplo.com',
      edad: 28
    });
    console.log(`Usuario agregado con ID: ${id}`);
  } catch (error) {
    console.error('Error:', error);
  }
}

// Consultar datos
async function buscarUsuarios() {
  try {
    // Obtener todos los usuarios
    const todosLosUsuarios = await db.usuarios.toArray();
    
    // Buscar por nombre
    const usuariosAna = await db.usuarios
      .where('nombre')
      .startsWith('Ana')
      .toArray();
    
    console.log('Usuarios encontrados:', usuariosAna);
  } catch (error) {
    console.error('Error:', error);
  }
}

Cache API

La Cache API permite almacenar respuestas HTTP para su uso posterior, incluso sin conexión. Es especialmente útil para implementar aplicaciones web progresivas (PWA).

// Abrir o crear una caché
caches.open('mi-cache-v1')
  .then(cache => {
    // Agregar recursos a la caché
    return cache.addAll([
      '/',
      '/css/estilos.css',
      '/js/app.js',
      '/imagenes/logo.png'
    ]);
  })
  .then(() => {
    console.log('Recursos almacenados en caché');
  })
  .catch(error => {
    console.error('Error al almacenar en caché:', error);
  });

// Recuperar un recurso de la caché
caches.match('/css/estilos.css')
  .then(response => {
    if (response) {
      return response;
    }
    throw new Error('Recurso no encontrado en caché');
  })
  .then(data => {
    console.log('Recurso recuperado de la caché');
  })
  .catch(error => {
    console.error('Error:', error);
  });

Comparativa de métodos de almacenamiento

CaracterísticaCookieslocalStoragesessionStorageIndexedDBCache API
Capacidad~4KB5-10MB5-10MB>50MBSegún dispositivo
PersistenciaConfigurablePermanenteSesiónPermanentePermanente
APIComplejaSimpleSimpleComplejaModerada
SincroníaSíncronaSíncronaSíncronaAsíncronaAsíncrona
Envío al servidorNoNoNoNo
Tipos de datosStringsStringsStringsCualquieraRespuestas HTTP
Caso de uso idealAutenticaciónPreferenciasEstado temporalDatos complejosRecursos offline

Mejores prácticas

Elegir el método adecuado

  • localStorage: Para preferencias de usuario, configuraciones y datos pequeños que deben persistir
  • sessionStorage: Para datos temporales durante una sesión de navegación
  • Cookies: Para datos que necesitan enviarse al servidor (autenticación)
  • IndexedDB: Para grandes cantidades de datos estructurados o aplicaciones offline
  • Cache API: Para almacenar recursos web (HTML, CSS, JS, imágenes)

Seguridad y privacidad

  1. No almacenar datos sensibles: Evita guardar contraseñas, tokens de acceso o información personal sin cifrar
  2. Validar datos: Siempre valida los datos antes de utilizarlos
  3. Manejar excepciones: El almacenamiento puede fallar (modo privado, espacio insuficiente)
  4. Respetar la privacidad: Informa a los usuarios sobre los datos que almacenas (política de cookies)
  5. Limpiar datos innecesarios: No acumules datos obsoletos

Rendimiento

  1. Minimizar operaciones de lectura/escritura: Especialmente con localStorage (operaciones síncronas)
  2. Agrupar operaciones: En lugar de múltiples setItem(), guarda objetos JSON
  3. Usar IndexedDB para operaciones frecuentes: Su naturaleza asíncrona no bloquea el hilo principal
  4. Implementar caducidad de datos: Elimina datos antiguos o no utilizados

Wrapper para Web Storage

Crear un wrapper puede simplificar el manejo de localStorage y sessionStorage:

const Storage = {
  // Guardar datos con expiración opcional
  set(key, value, expirationMinutes = null, useSession = false) {
    const storage = useSession ? sessionStorage : localStorage;
    
    const item = {
      value: value,
      timestamp: new Date().getTime()
    };
    
    if (expirationMinutes) {
      item.expiration = expirationMinutes * 60 * 1000;
    }
    
    storage.setItem(key, JSON.stringify(item));
  },
  
  // Obtener datos verificando expiración
  get(key, useSession = false) {
    const storage = useSession ? sessionStorage : localStorage;
    const itemStr = storage.getItem(key);
    
    if (!itemStr) return null;
    
    try {
      const item = JSON.parse(itemStr);
      
      // Verificar si el dato ha expirado
      if (item.expiration) {
        const now = new Date().getTime();
        const expiresAt = item.timestamp + item.expiration;
        
        if (now > expiresAt) {
          this.remove(key, useSession);
          return null;
        }
      }
      
      return item.value;
    } catch (e) {
      console.error('Error al parsear datos:', e);
      return null;
    }
  },
  
  // Eliminar datos
  remove(key, useSession = false) {
    const storage = useSession ? sessionStorage : localStorage;
    storage.removeItem(key);
  },
  
  // Limpiar todo el almacenamiento
  clear(useSession = false) {
    const storage = useSession ? sessionStorage : localStorage;
    storage.clear();
  }
};

// Uso
Storage.set('usuario', { nombre: 'Ana', id: 123 }, 60); // Expira en 60 minutos
Storage.set('carrito', [{ id: 1, cantidad: 2 }], null, true); // En sessionStorage

const usuario = Storage.get('usuario');
console.log(usuario); // { nombre: 'Ana', id: 123 }

Ejemplo práctico: Aplicación de notas

// Modelo de datos
const NotasApp = {
  // Inicializar la aplicación
  init() {
    this.cargarNotas();
    this.bindEventos();
  },
  
  // Cargar notas desde localStorage
  cargarNotas() {
    const notasGuardadas = localStorage.getItem('notas');
    this.notas = notasGuardadas ? JSON.parse(notasGuardadas) : [];
    this.renderizarNotas();
  },
  
  // Guardar notas en localStorage
  guardarNotas() {
    localStorage.setItem('notas', JSON.stringify(this.notas));
  },
  
  // Agregar una nueva nota
  agregarNota(titulo, contenido) {
    const nuevaNota = {
      id: Date.now(),
      titulo,
      contenido,
      fecha: new Date().toISOString()
    };
    
    this.notas.push(nuevaNota);
    this.guardarNotas();
    this.renderizarNotas();
  },
  
  // Eliminar una nota
  eliminarNota(id) {
    this.notas = this.notas.filter(nota => nota.id !== id);
    this.guardarNotas();
    this.renderizarNotas();
  },
  
  // Renderizar notas en el DOM
  renderizarNotas() {
    const contenedor = document.getElementById('notas-container');
    contenedor.innerHTML = '';
    
    if (this.notas.length === 0) {
      contenedor.innerHTML = '<p class="sin-notas">No hay notas guardadas</p>';
      return;
    }
    
    this.notas.forEach(nota => {
      const fecha = new Date(nota.fecha).toLocaleDateString();
      
      const notaEl = document.createElement('div');
      notaEl.className = 'nota';
      notaEl.innerHTML = `
        <h3>${nota.titulo}</h3>
        <p>${nota.contenido}</p>
        <div class="nota-footer">
          <span class="fecha">${fecha}</span>
          <button class="eliminar" data-id="${nota.id}">Eliminar</button>
        </div>
      `;
      
      contenedor.appendChild(notaEl);
    });
    
    // Agregar eventos a los botones de eliminar
    document.querySelectorAll('.eliminar').forEach(btn => {
      btn.addEventListener('click', (e) => {
        const id = parseInt(e.target.dataset.id);
        this.eliminarNota(id);
      });
    });
  },
  
  // Vincular eventos
  bindEventos() {
    const formulario = document.getElementById('formulario-nota');
    
    formulario.addEventListener('submit', (e) => {
      e.preventDefault();
      
      const titulo = document.getElementById('titulo').value.trim();
      const contenido = document.getElementById('contenido').value.trim();
      
      if (!titulo || !contenido) {
        alert('Por favor completa todos los campos');
        return;
      }
      
      this.agregarNota(titulo, contenido);
      formulario.reset();
    });
  }
};

// Inicializar la aplicación cuando el DOM esté listo
document.addEventListener('DOMContentLoaded', () => {
  NotasApp.init();
});

HTML correspondiente:

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Aplicación de Notas</title>
  <style>
    /* Estilos básicos */
    body {
      font-family: Arial, sans-serif;
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
    }
    .formulario {
      margin-bottom: 20px;
      padding: 15px;
      background-color: #f5f5f5;
      border-radius: 5px;
    }
    .campo {
      margin-bottom: 10px;
    }
    label {
      display: block;
      margin-bottom: 5px;
    }
    input, textarea {
      width: 100%;
      padding: 8px;
      box-sizing: border-box;
    }
    button {
      padding: 8px 15px;
      background-color: #4CAF50;
      color: white;
      border: none;
      cursor: pointer;
    }
    .notas {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
      gap: 15px;
    }
    .nota {
      padding: 15px;
      background-color: #fff9c4;
      border-radius: 5px;
      box-shadow: 0 2px 5px rgba(0,0,0,0.1);
    }
    .nota-footer {
      display: flex;
      justify-content: space-between;
      margin-top: 15px;
      font-size: 0.8em;
    }
    .eliminar {
      background-color: #f44336;
      padding: 5px 10px;
      font-size: 0.8em;
    }
  </style>
</head>
<body>
  <h1>Mis Notas</h1>
  
  <div class="formulario" id="formulario-nota">
    <div class="campo">
      <label for="titulo">Título</label>
      <input type="text" id="titulo" required>
    </div>
    <div class="campo">
      <label for="contenido">Contenido</label>
      <textarea id="contenido" rows="4" required></textarea>
    </div>
    <button type="submit">Guardar Nota</button>
  </div>
  
  <h2>Notas Guardadas</h2>
  <div class="notas" id="notas-container"></div>
  
  <script src="app.js"></script>
</body>
</html>
Fetch API y AJAX
Aprende a realizar peticiones HTTP desde JavaScript para com...
Programación Orientada a Objetos
Aprende a organizar tu código utilizando el paradigma orient...

Conceptos Básicos de HTML

Aprende los conceptos básicos de HTML

Conceptos Básicos de CSS

Aprende los conceptos básicos de CSS

Conceptos Básicos SQL

Aprende los conceptos básicos de SQL

Conceptos Básicos de GIT

Aprende los conceptos básicos de GIT

Conceptos Básicos de Python

Aprende los conceptos básicos de Python

Conceptos Básicos de UML

Aprende los conceptos básicos de UML

Refuerzo Academico de Herramientas de Productividad 2025