JavaScript
Tutorial JavaScript: LocalStorage y SessionStorage
Descubre los límites y seguridad en el almacenamiento web para aplicaciones modernas. Aprende a usar APIs eficientemente con medidas de protección.
Aprende JavaScript y certifícateLímites y consideraciones de seguridad
Al trabajar con las APIs de almacenamiento web (localStorage y sessionStorage), es fundamental comprender sus limitaciones técnicas y las implicaciones de seguridad que conllevan. Estos mecanismos, aunque útiles, no son soluciones universales para todos los escenarios de persistencia de datos.
Capacidad de almacenamiento
La capacidad de almacenamiento disponible en localStorage y sessionStorage está limitada por los navegadores. Aunque esta limitación varía según el navegador, generalmente se establece alrededor de 5-10 MB por dominio. Esta restricción es importante cuando planificamos qué datos almacenar:
// Función para comprobar el espacio utilizado en localStorage
function checkStorageUsage() {
let totalSize = 0;
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
totalSize += localStorage[key].length * 2; // Caracteres * 2 bytes (UTF-16)
}
}
return (totalSize / 1024).toFixed(2) + " KB";
}
console.log(`Espacio utilizado: ${checkStorageUsage()}`);
Cuando intentamos exceder este límite, el navegador lanzará una excepción. Es buena práctica implementar manejo de errores para estas situaciones:
try {
// Intentamos almacenar un objeto grande
const largeObject = { /* datos extensos */ };
localStorage.setItem('largeData', JSON.stringify(largeObject));
} catch (e) {
if (e instanceof DOMException && e.name === 'QuotaExceededError') {
console.error('Error: Límite de almacenamiento excedido');
// Implementar estrategia de gestión (eliminar datos antiguos, etc.)
}
}
Políticas de mismo origen (Same-Origin Policy)
Una de las restricciones de seguridad más importantes es la política de mismo origen. Tanto localStorage como sessionStorage están vinculados al origen del documento, que se define por la combinación de:
- Protocolo (http, https)
- Dominio (ejemplo.com)
- Puerto (80, 443, etc.)
// Este almacenamiento solo es accesible desde https://miapp.com:443
// No puede ser accedido desde http://miapp.com o https://otrositio.com
localStorage.setItem('userData', JSON.stringify({name: 'Ana'}));
Esta restricción tiene importantes implicaciones:
- Los datos almacenados en
https://app.ejemplo.com
no son accesibles desdehttps://ejemplo.com
- Subdominios diferentes se consideran orígenes distintos
- El cambio de protocolo (HTTP a HTTPS) crea un origen diferente
Vulnerabilidades de seguridad
El almacenamiento web no está cifrado por defecto, lo que presenta varios riesgos de seguridad:
- Ataques XSS: Un script malicioso inyectado en la página puede acceder a todo el almacenamiento del dominio:
// Código potencialmente vulnerable
function displayUserComment(comment) {
// Peligroso: insertar contenido sin sanitizar
document.getElementById('comments').innerHTML += comment;
// Mejor enfoque: sanitizar el contenido
const sanitizedComment = DOMPurify.sanitize(comment);
document.getElementById('comments').innerHTML += sanitizedComment;
}
- Persistencia de datos sensibles: Nunca debemos almacenar información confidencial sin cifrado:
// Enfoque incorrecto
localStorage.setItem('authToken', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');
// Mejor enfoque (aunque no perfecto)
function encryptData(data, key) {
// Implementación de cifrado (usando SubtleCrypto API)
return encryptedData;
}
const encryptedToken = encryptData(token, secretKey);
localStorage.setItem('secureToken', encryptedToken);
Modo incógnito y limpieza de navegador
Es importante considerar que los datos en localStorage persisten incluso después de cerrar el navegador, pero pueden ser eliminados en estas situaciones:
- Cuando el usuario borra los datos de navegación
- En modo incógnito/privado, donde los datos se eliminan al cerrar la ventana
- Si el usuario tiene configurado su navegador para rechazar cookies o almacenamiento local
// Detectar si localStorage está disponible
function isStorageAvailable() {
try {
const testKey = '__storage_test__';
localStorage.setItem(testKey, testKey);
localStorage.removeItem(testKey);
return true;
} catch (e) {
return false;
}
}
// Implementar alternativa si no está disponible
if (!isStorageAvailable()) {
console.warn('localStorage no disponible, usando alternativa en memoria');
// Implementar solución alternativa
}
Consideraciones de rendimiento
Aunque el acceso al almacenamiento web es síncrono, operaciones frecuentes o con grandes volúmenes de datos pueden afectar el rendimiento:
- Las operaciones de lectura/escritura bloquean el hilo principal
- La serialización/deserialización de objetos complejos puede ser costosa
// Enfoque ineficiente: múltiples operaciones individuales
for (let i = 0; i < 1000; i++) {
localStorage.setItem(`item_${i}`, `value_${i}`);
}
// Enfoque más eficiente: operación única con estructura de datos
const batchData = {};
for (let i = 0; i < 1000; i++) {
batchData[`item_${i}`] = `value_${i}`;
}
localStorage.setItem('batchItems', JSON.stringify(batchData));
Comprender estas limitaciones y consideraciones de seguridad es esencial para implementar soluciones de almacenamiento web robustas y seguras en aplicaciones JavaScript modernas.
Patrones de diseño: Implementación de wrapper classes y sistemas de caché
El uso directo de localStorage y sessionStorage puede volverse complejo cuando las aplicaciones crecen. Para mejorar la mantenibilidad, testabilidad y agregar funcionalidades avanzadas, es recomendable implementar patrones de diseño que encapsulen estas APIs nativas. Veremos cómo crear abstracciones elegantes que simplifiquen el trabajo con el almacenamiento web.
Wrapper classes para almacenamiento
Una clase wrapper proporciona una interfaz más amigable sobre las APIs nativas, añadiendo funcionalidades como manejo automático de JSON, expiración de datos y validación:
class StorageManager {
constructor(storageType = 'local') {
this.storage = storageType === 'local' ? localStorage : sessionStorage;
}
set(key, value, expiresIn = null) {
const item = {
value,
timestamp: Date.now()
};
if (expiresIn) {
item.expiresAt = Date.now() + expiresIn;
}
this.storage.setItem(key, JSON.stringify(item));
return true;
}
get(key) {
const item = this.storage.getItem(key);
if (!item) return null;
const parsedItem = JSON.parse(item);
// Verificar expiración
if (parsedItem.expiresAt && parsedItem.expiresAt < Date.now()) {
this.remove(key);
return null;
}
return parsedItem.value;
}
remove(key) {
this.storage.removeItem(key);
}
clear() {
this.storage.clear();
}
}
Esta implementación ofrece varias ventajas:
- Manejo automático de serialización/deserialización JSON
- Expiración de datos configurable
- Interfaz unificada para localStorage y sessionStorage
- Encapsulación que facilita cambios futuros
Podemos utilizar esta clase de manera sencilla:
const userStorage = new StorageManager('local');
// Guardar datos con expiración de 1 hora
userStorage.set('userProfile', { name: 'Carlos', role: 'admin' }, 3600000);
// Recuperar datos
const profile = userStorage.get('userProfile');
console.log(profile); // { name: 'Carlos', role: 'admin' }
Sistemas de caché con almacenamiento web
Implementar un sistema de caché es otro patrón común que mejora el rendimiento de aplicaciones que realizan operaciones costosas o peticiones a APIs:
class CacheService {
constructor(defaultExpiration = 3600000) { // 1 hora por defecto
this.storage = new StorageManager('local');
this.defaultExpiration = defaultExpiration;
}
async fetch(url, options = {}) {
const cacheKey = `cache_${this.hashUrl(url)}`;
const cachedResponse = this.storage.get(cacheKey);
if (cachedResponse) {
return cachedResponse;
}
try {
const response = await fetch(url, options);
const data = await response.json();
// Guardar en caché
this.storage.set(
cacheKey,
data,
options.expiration || this.defaultExpiration
);
return data;
} catch (error) {
console.error('Error fetching data:', error);
throw error;
}
}
invalidate(url) {
const cacheKey = `cache_${this.hashUrl(url)}`;
this.storage.remove(cacheKey);
}
hashUrl(url) {
// Implementación simple de hash para URLs
let hash = 0;
for (let i = 0; i < url.length; i++) {
hash = ((hash << 5) - hash) + url.charCodeAt(i);
hash |= 0; // Convertir a entero de 32 bits
}
return hash.toString(16);
}
}
Este sistema de caché ofrece beneficios significativos:
- Reducción de peticiones a servidores externos
- Mejora de rendimiento para operaciones repetitivas
- Funcionamiento offline para datos previamente cargados
- Control granular sobre la invalidación de caché
Ejemplo de uso del sistema de caché:
const apiCache = new CacheService();
// Componente que muestra productos
async function loadProducts() {
try {
// Los datos se cargarán desde la caché si están disponibles
const products = await apiCache.fetch('https://api.example.com/products');
renderProductList(products);
} catch (error) {
showErrorMessage('No se pudieron cargar los productos');
}
}
// Invalidar caché cuando se actualiza un producto
function updateProduct(productId, data) {
// Actualizar en el servidor
fetch(`https://api.example.com/products/${productId}`, {
method: 'PUT',
body: JSON.stringify(data)
}).then(() => {
// Invalidar la caché para forzar una recarga fresca
apiCache.invalidate('https://api.example.com/products');
});
}
Patrón Repository con almacenamiento local
El patrón Repository proporciona una abstracción sobre el mecanismo de almacenamiento, permitiendo operaciones CRUD consistentes independientemente del origen de datos:
class UserRepository {
constructor() {
this.storage = new StorageManager('local');
this.collectionKey = 'users';
}
getAll() {
return this.storage.get(this.collectionKey) || [];
}
getById(id) {
const users = this.getAll();
return users.find(user => user.id === id) || null;
}
save(user) {
const users = this.getAll();
const existingIndex = users.findIndex(u => u.id === user.id);
if (existingIndex >= 0) {
// Actualizar usuario existente
users[existingIndex] = { ...users[existingIndex], ...user };
} else {
// Crear nuevo usuario con ID generado
users.push({
...user,
id: this.generateId(),
createdAt: Date.now()
});
}
this.storage.set(this.collectionKey, users);
return user;
}
delete(id) {
const users = this.getAll();
const filteredUsers = users.filter(user => user.id !== id);
if (filteredUsers.length !== users.length) {
this.storage.set(this.collectionKey, filteredUsers);
return true;
}
return false;
}
generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
}
Este patrón facilita la gestión de colecciones de datos y proporciona una interfaz uniforme para operaciones CRUD:
const userRepo = new UserRepository();
// Crear usuario
const newUser = userRepo.save({ name: 'Elena', email: 'elena@example.com' });
// Obtener todos los usuarios
const allUsers = userRepo.getAll();
// Actualizar usuario
userRepo.save({ id: newUser.id, role: 'editor' });
// Eliminar usuario
userRepo.delete(newUser.id);
La implementación de estos patrones de diseño mejora significativamente la arquitectura de aplicaciones que utilizan almacenamiento web, facilitando el mantenimiento, las pruebas y la escalabilidad del código. Además, proporcionan una capa de abstracción que permite cambiar la implementación subyacente sin afectar al código cliente.
Sincronización entre pestañas: Detección de cambios con el evento storage
Cuando trabajamos con aplicaciones web modernas, es común que los usuarios tengan múltiples pestañas abiertas de la misma aplicación. En estos escenarios, mantener la sincronización de datos entre estas pestañas se vuelve crucial para garantizar una experiencia coherente. JavaScript proporciona un mecanismo elegante para detectar cambios en el almacenamiento web a través del evento storage.
El evento storage
se dispara automáticamente en todas las pestañas o ventanas (excepto en la que realizó el cambio) cuando se modifica el localStorage. Esta característica permite implementar sistemas de comunicación entre diferentes instancias de nuestra aplicación sin necesidad de servidores intermedios.
// Escuchar cambios en localStorage desde otras pestañas
window.addEventListener('storage', (event) => {
console.log('Cambio detectado en otra pestaña:');
console.log('Clave modificada:', event.key);
console.log('Valor anterior:', event.oldValue);
console.log('Nuevo valor:', event.oldValue);
console.log('URL de la pestaña que realizó el cambio:', event.url);
});
El objeto event
proporciona información detallada sobre el cambio realizado:
key
: La clave que se modificóoldValue
: El valor anterior (antes del cambio)newValue
: El nuevo valor establecidourl
: La URL de la página donde se realizó el cambiostorageArea
: Referencia al objeto de almacenamiento (localStorage o sessionStorage)
Implementación de un sistema de notificaciones entre pestañas
Podemos aprovechar este mecanismo para crear un sistema de notificaciones que mantenga sincronizadas todas las instancias de nuestra aplicación:
class TabSynchronizer {
constructor(channelName = 'app-sync') {
this.channel = channelName;
this.listeners = new Map();
// Escuchar eventos de otras pestañas
window.addEventListener('storage', (event) => {
if (event.key === this.channel) {
try {
const data = JSON.parse(event.newValue);
if (data && data.type && this.listeners.has(data.type)) {
this.listeners.get(data.type).forEach(callback => {
callback(data.payload);
});
}
} catch (error) {
console.error('Error al procesar mensaje entre pestañas:', error);
}
}
});
}
// Enviar mensaje a otras pestañas
broadcast(type, payload) {
const message = JSON.stringify({
type,
payload,
timestamp: Date.now()
});
localStorage.setItem(this.channel, message);
// Necesario para que otras pestañas detecten cambios consecutivos
// con el mismo tipo de mensaje
setTimeout(() => localStorage.removeItem(this.channel), 100);
}
// Suscribirse a un tipo de mensaje
subscribe(type, callback) {
if (!this.listeners.has(type)) {
this.listeners.set(type, new Set());
}
this.listeners.get(type).add(callback);
// Devolver función para cancelar suscripción
return () => {
const callbacks = this.listeners.get(type);
callbacks.delete(callback);
if (callbacks.size === 0) {
this.listeners.delete(type);
}
};
}
}
Este sistema permite implementar comunicación bidireccional entre pestañas de forma sencilla:
// Crear instancia del sincronizador
const tabSync = new TabSynchronizer();
// Suscribirse a actualizaciones de usuario
const unsubscribe = tabSync.subscribe('user-updated', (userData) => {
console.log('Datos de usuario actualizados en otra pestaña:', userData);
updateUserInterface(userData);
});
// Cuando el usuario actualiza su perfil
function updateUserProfile(newData) {
// Actualizar en el servidor
api.updateProfile(newData)
.then(response => {
// Guardar localmente
localStorage.setItem('user', JSON.stringify(response.data));
// Notificar a otras pestañas
tabSync.broadcast('user-updated', response.data);
showSuccessMessage('Perfil actualizado correctamente');
});
}
Sincronización de estado de autenticación
Un caso de uso común es mantener sincronizado el estado de autenticación entre pestañas. Cuando un usuario cierra sesión en una pestaña, probablemente desee que todas las demás pestañas también reflejen este cambio:
class AuthSynchronizer {
constructor() {
this.tabSync = new TabSynchronizer('auth-channel');
// Escuchar eventos de autenticación
this.tabSync.subscribe('auth-changed', (authState) => {
if (authState.status === 'logged-out') {
// Limpiar datos locales
localStorage.removeItem('authToken');
localStorage.removeItem('userData');
// Redirigir a página de login
window.location.href = '/login?reason=session_ended';
}
});
}
// Llamar cuando el usuario cierra sesión manualmente
logout() {
// Limpiar datos de autenticación
localStorage.removeItem('authToken');
localStorage.removeItem('userData');
// Notificar a otras pestañas
this.tabSync.broadcast('auth-changed', {
status: 'logged-out',
timestamp: Date.now()
});
// Redirigir a login
window.location.href = '/login';
}
}
// Uso
const authSync = new AuthSynchronizer();
document.getElementById('logout-button').addEventListener('click', () => {
authSync.logout();
});
Sincronización de carrito de compras
Otro escenario común es la sincronización de un carrito de compras entre diferentes pestañas de una tienda online:
class CartSynchronizer {
constructor() {
this.tabSync = new TabSynchronizer('cart-channel');
// Inicializar carrito desde localStorage
this.cart = JSON.parse(localStorage.getItem('cart')) || { items: [], total: 0 };
// Escuchar cambios en el carrito desde otras pestañas
this.tabSync.subscribe('cart-updated', (newCart) => {
this.cart = newCart;
this.updateCartUI();
});
}
// Añadir producto al carrito
addToCart(product, quantity = 1) {
// Buscar si el producto ya existe en el carrito
const existingItem = this.cart.items.find(item => item.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.cart.items.push({
id: product.id,
name: product.name,
price: product.price,
quantity
});
}
// Recalcular total
this.cart.total = this.cart.items.reduce(
(sum, item) => sum + (item.price * item.quantity), 0
);
// Guardar en localStorage
localStorage.setItem('cart', JSON.stringify(this.cart));
// Notificar a otras pestañas
this.tabSync.broadcast('cart-updated', this.cart);
// Actualizar interfaz
this.updateCartUI();
}
updateCartUI() {
// Actualizar contador de items
document.querySelector('.cart-count').textContent =
this.cart.items.reduce((sum, item) => sum + item.quantity, 0);
// Actualizar total
document.querySelector('.cart-total').textContent =
`$${this.cart.total.toFixed(2)}`;
// Actualizar lista de productos si está visible
const cartList = document.querySelector('.cart-items');
if (cartList) {
// Implementación de actualización del DOM
}
}
}
Consideraciones y limitaciones
Al implementar sistemas de sincronización entre pestañas, es importante tener en cuenta algunas limitaciones:
- El evento
storage
no se dispara en la misma pestaña que realizó el cambio - Existe un límite de tamaño para los datos que podemos almacenar
- La comunicación es asíncrona pero no garantiza entrega inmediata
- No funciona entre dominios diferentes debido a la política de mismo origen
Para casos más complejos donde estas limitaciones sean problemáticas, podemos considerar alternativas como WebSockets o la API BroadcastChannel:
// Alternativa usando BroadcastChannel (no soportada en todos los navegadores)
class ModernTabSynchronizer {
constructor(channelName) {
this.channel = new BroadcastChannel(channelName);
this.listeners = new Map();
this.channel.addEventListener('message', (event) => {
const { type, payload } = event.data;
if (this.listeners.has(type)) {
this.listeners.get(type).forEach(callback => callback(payload));
}
});
}
broadcast(type, payload) {
this.channel.postMessage({ type, payload, timestamp: Date.now() });
}
subscribe(type, callback) {
if (!this.listeners.has(type)) {
this.listeners.set(type, new Set());
}
this.listeners.get(type).add(callback);
return () => {
const callbacks = this.listeners.get(type);
callbacks.delete(callback);
if (callbacks.size === 0) {
this.listeners.delete(type);
}
};
}
}
La implementación de sistemas de sincronización entre pestañas mejora significativamente la experiencia de usuario en aplicaciones web modernas, garantizando coherencia de datos y reduciendo la confusión que puede surgir cuando diferentes instancias de la misma aplicación muestran estados diferentes.
Otras lecciones de JavaScript
Accede a todas las lecciones de JavaScript y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Javascript
Introducción Y Entorno
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Ejercicios de programación de JavaScript
Evalúa tus conocimientos de esta lección LocalStorage y SessionStorage con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.