Almacenamiento en el Navegador
Aprende a almacenar datos en el navegador utilizando localStorage, sessionStorage y cookies para crear aplicaciones web con persistencia de datos.
Cristian Escalante
Última actualización: 22 de abril de 2025
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ística | Cookies | localStorage | sessionStorage | IndexedDB | Cache API |
---|---|---|---|---|---|
Capacidad | ~4KB | 5-10MB | 5-10MB | >50MB | Según dispositivo |
Persistencia | Configurable | Permanente | Sesión | Permanente | Permanente |
API | Compleja | Simple | Simple | Compleja | Moderada |
Sincronía | Síncrona | Síncrona | Síncrona | Asíncrona | Asíncrona |
Envío al servidor | Sí | No | No | No | No |
Tipos de datos | Strings | Strings | Strings | Cualquiera | Respuestas HTTP |
Caso de uso ideal | Autenticación | Preferencias | Estado temporal | Datos complejos | Recursos 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
- No almacenar datos sensibles: Evita guardar contraseñas, tokens de acceso o información personal sin cifrar
- Validar datos: Siempre valida los datos antes de utilizarlos
- Manejar excepciones: El almacenamiento puede fallar (modo privado, espacio insuficiente)
- Respetar la privacidad: Informa a los usuarios sobre los datos que almacenas (política de cookies)
- Limpiar datos innecesarios: No acumules datos obsoletos
Rendimiento
- Minimizar operaciones de lectura/escritura: Especialmente con localStorage (operaciones síncronas)
- Agrupar operaciones: En lugar de múltiples setItem(), guarda objetos JSON
- Usar IndexedDB para operaciones frecuentes: Su naturaleza asíncrona no bloquea el hilo principal
- 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>