Ir al contenido principal

LocalStorage y SessionStorage

Introducción

El almacenamiento web es una de las características más útiles y prácticas que los navegadores modernos ponen a nuestra disposición. A diferencia de las cookies, que tienen limitaciones significativas, las API de almacenamiento web nos permiten guardar datos en el navegador del usuario de manera eficiente y con un mayor control. JavaScript proporciona dos mecanismos principales para esto: localStorage y sessionStorage. Estos sistemas de almacenamiento permiten a las aplicaciones web guardar información en el dispositivo del usuario, lo que resulta útil para mejorar el rendimiento, permitir el uso sin conexión y proporcionar una experiencia personalizada.

Concepto de almacenamiento web

El almacenamiento web proporciona a las aplicaciones web la capacidad de guardar datos en el navegador del usuario en formato de pares clave-valor. Es una alternativa más moderna a las cookies, ofreciendo mayor capacidad de almacenamiento y una API más sencilla de utilizar. Los datos almacenados son específicos del origen (dominio, protocolo y puerto), lo que significa que un sitio web no puede acceder a los datos almacenados por otro sitio web, garantizando así la seguridad.

Diferencias entre localStorage y sessionStorage

Aunque ambos métodos comparten la misma API y propósito general, existen diferencias cruciales en su comportamiento y duración:

localStorage

  • Persistencia: Los datos almacenados en localStorage persisten incluso después de cerrar el navegador o apagar el ordenador.
  • Duración: No tienen fecha de expiración. Los datos permanecen hasta que son eliminados explícitamente por el código o por el usuario (limpiando los datos del navegador).
  • Alcance: La información es compartida entre todas las pestañas y ventanas del mismo origen.
  • Caso de uso típico: Preferencias de usuario, configuración de la aplicación, datos de autenticación (con precaución), contenido guardado que debe persistir a largo plazo.

sessionStorage

  • Persistencia: Los datos solo persisten durante la sesión de navegación actual.
  • Duración: Al cerrar la pestaña o ventana, los datos se eliminan automáticamente.
  • Alcance: La información es exclusiva de la pestaña o ventana donde se creó. Otras pestañas, incluso del mismo origen, no comparten estos datos.
  • Caso de uso típico: Estado temporal de formularios, datos de paso en procesos de varios pasos, historial de navegación de la sesión actual.

Métodos comunes

Tanto localStorage como sessionStorage comparten la misma interfaz, proporcionando los siguientes métodos:

setItem(clave, valor)

Este método almacena un par clave-valor en el almacenamiento.

// Guardar datos en localStorage
localStorage.setItem('nombre', 'Carlos');
localStorage.setItem('tema', 'oscuro');

// Guardar datos en sessionStorage
sessionStorage.setItem('ultimaPagina', 'contacto');
sessionStorage.setItem('formularioCompleto', 'false');

getItem(clave)

Recupera el valor asociado a una clave específica. Si la clave no existe, devuelve null.

// Recuperar datos de localStorage
const nombre = localStorage.getItem('nombre'); // "Carlos"
const tema = localStorage.getItem('tema');     // "oscuro"

// Recuperar datos de sessionStorage
const ultimaPagina = sessionStorage.getItem('ultimaPagina'); // "contacto"
const formularioCompleto = sessionStorage.getItem('formularioCompleto'); // "false"

// Acceder a una clave inexistente
const edad = localStorage.getItem('edad'); // null

removeItem(clave)

Elimina el par clave-valor asociado a la clave especificada.

// Eliminar un elemento de localStorage
localStorage.removeItem('tema');

// Eliminar un elemento de sessionStorage
sessionStorage.removeItem('formularioCompleto');

clear()

Elimina todos los pares clave-valor del almacenamiento.

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

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

Propiedad length

Devuelve el número de elementos almacenados.

// Contar elementos en localStorage
const cantidadLocal = localStorage.length;

// Contar elementos en sessionStorage
const cantidadSesion = sessionStorage.length;

key(índice)

Devuelve la clave almacenada en la posición especificada.

// Obtener la primera clave almacenada en localStorage
const primeraClave = localStorage.key(0);

// Ejemplo de iteración sobre todas las claves y valores
for (let i = 0; i < localStorage.length; i++) {
  const clave = localStorage.key(i);
  const valor = localStorage.getItem(clave);
  console.log(`${clave}: ${valor}`);
}

Almacenamiento de objetos con JSON

Una limitación importante de estas APIs es que solo pueden almacenar strings. Para guardar objetos o arrays, necesitamos convertirlos a formato JSON y luego recuperarlos.

// Objeto que queremos almacenar
const usuario = {
  id: 1,
  nombre: 'Ana',
  apellidos: 'García López',
  preferencias: {
    tema: 'claro',
    notificaciones: true
  }
};

// Guardar el objeto en localStorage
localStorage.setItem('usuario', JSON.stringify(usuario));

// Recuperar y convertir de nuevo a objeto
const usuarioRecuperado = JSON.parse(localStorage.getItem('usuario'));
console.log(usuarioRecuperado.nombre); // "Ana"
console.log(usuarioRecuperado.preferencias.tema); // "claro"

Este patrón es extremadamente útil y común en aplicaciones web modernas. Sin embargo, hay que tener en cuenta que se producirá un error si intentamos usar JSON.parse() con un valor que no es un JSON válido:

// Esto provocará un error
try {
  const dato = JSON.parse(localStorage.getItem('claveInexistente'));
} catch (error) {
  console.error('Error al parsear JSON:', error);
}

// La forma segura de hacerlo
const datoJSON = localStorage.getItem('claveInexistente');
let dato = null;
if (datoJSON) {
  try {
    dato = JSON.parse(datoJSON);
  } catch (error) {
    console.error('El valor no es un JSON válido:', error);
  }
}

Limitaciones de almacenamiento

Es importante conocer las limitaciones de estas APIs:

  1. Espacio disponible: Generalmente entre 5MB y 10MB por origen, según el navegador.
  2. Solo almacena strings: Para otros tipos de datos, se debe usar JSON.stringify().
  3. Operaciones síncronas: Las operaciones bloquean el hilo principal, lo que puede afectar al rendimiento con grandes volúmenes de datos.
  4. No está disponible en modo incógnito en algunos navegadores o puede borrarse al cerrar la sesión.
  5. No es seguro para datos sensibles: No debe utilizarse para información confidencial sin encriptación adicional.
// Función para comprobar el espacio disponible aproximado
function comprobarEspacioDisponible() {
  const testKey = 'test_espacio';
  try {
    // Limpiamos cualquier test anterior
    localStorage.removeItem(testKey);
    
    // Intentamos almacenar bloques de 100KB hasta que falle
    let size = 0;
    const block = '0'.repeat(100 * 1024); // 100KB
    
    while (true) {
      localStorage.setItem(testKey, localStorage.getItem(testKey) + block);
      size += 100;
      if (size > 10000) { // Límite seguro para no bloquear el navegador
        break;
      }
    }
  } catch (e) {
    console.log(`Espacio disponible aproximado: ${size} KB`);
  } finally {
    // Limpiamos nuestro test
    localStorage.removeItem(testKey);
  }
}

Eventos de almacenamiento

Cuando los datos en localStorage cambian en una pestaña, se dispara un evento storage en otras pestañas del mismo origen. Este evento no se dispara en la pestaña que hizo el cambio, y no funciona con sessionStorage ya que este es específico de cada pestaña.

// Escuchar cambios en localStorage desde otras pestañas
window.addEventListener('storage', function(event) {
  console.log('Cambio detectado en localStorage:');
  console.log('Clave modificada:', event.key);
  console.log('Valor anterior:', event.oldValue);
  console.log('Nuevo valor:', event.newValue);
  console.log('URL de la página que hizo el cambio:', event.url);
  
  // Podemos actuar según la clave modificada
  if (event.key === 'tema') {
    aplicarTema(event.newValue);
  }
});

function aplicarTema(tema) {
  // Lógica para cambiar el tema de la aplicación
  document.body.className = tema;
  console.log(`Tema cambiado a: ${tema}`);
}

Este mecanismo es muy útil para sincronizar el estado entre diferentes pestañas de la misma aplicación.

Casos de uso apropiados

El almacenamiento web es ideal para ciertos escenarios:

Para localStorage:

  1. Preferencias de usuario persistentes:
// Guardar preferencias
function guardarPreferencias(tema, tamanoFuente, idioma) {
  const preferencias = {
    tema,
    tamanoFuente,
    idioma,
    fechaActualizacion: new Date().toISOString()
  };
  localStorage.setItem('preferenciasUsuario', JSON.stringify(preferencias));
}

// Cargar preferencias
function cargarPreferencias() {
  const prefJSON = localStorage.getItem('preferenciasUsuario');
  return prefJSON ? JSON.parse(prefJSON) : null;
}
  1. Caché de datos para mejorar rendimiento:
async function obtenerProductos() {
  // Intentar recuperar del caché
  const cacheProductos = localStorage.getItem('catalogoProductos');
  const cacheTiempo = localStorage.getItem('catalogoTiempo');
  
  // Si hay un caché válido (menos de 1 hora)
  if (cacheProductos && cacheTiempo) {
    const ahora = new Date().getTime();
    const tiempoCache = parseInt(cacheTiempo);
    
    // Caché válido por 1 hora (3600000 ms)
    if (ahora - tiempoCache < 3600000) {
      return JSON.parse(cacheProductos);
    }
  }
  
  // Si no hay caché o expiró, hacer la petición
  try {
    const respuesta = await fetch('https://api.ejemplo.com/productos');
    const productos = await respuesta.json();
    
    // Guardar en caché
    localStorage.setItem('catalogoProductos', JSON.stringify(productos));
    localStorage.setItem('catalogoTiempo', new Date().getTime().toString());
    
    return productos;
  } catch (error) {
    console.error('Error al obtener productos:', error);
    // Si hay error pero tenemos caché, lo usamos aunque sea antiguo
    if (cacheProductos) {
      return JSON.parse(cacheProductos);
    }
    throw error;
  }
}
  1. Datos para funcionamiento offline:
// Guardar notas para uso offline
function guardarNota(titulo, contenido) {
  // Recuperar notas existentes o inicializar array
  const notasJSON = localStorage.getItem('notasOffline');
  const notas = notasJSON ? JSON.parse(notasJSON) : [];
  
  // Añadir nueva nota
  notas.push({
    id: Date.now(), // Timestamp como identificador único
    titulo,
    contenido,
    fechaCreacion: new Date().toISOString(),
    sincronizado: false
  });
  
  // Guardar notas actualizadas
  localStorage.setItem('notasOffline', JSON.stringify(notas));
}

// Sincronizar cuando hay conexión
async function sincronizarNotas() {
  if (!navigator.onLine) {
    return {estado: 'offline', mensaje: 'Sin conexión. Se sincronizará más tarde.'};
  }
  
  const notasJSON = localStorage.getItem('notasOffline');
  if (!notasJSON) return {estado: 'completado', mensaje: 'No hay notas para sincronizar'};
  
  const notas = JSON.parse(notasJSON);
  const notasPendientes = notas.filter(nota => !nota.sincronizado);
  
  if (notasPendientes.length === 0) {
    return {estado: 'completado', mensaje: 'No hay notas pendientes'};
  }
  
  try {
    // Ejemplo de sincronización con servidor
    const respuesta = await fetch('https://api.ejemplo.com/sincronizar-notas', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify(notasPendientes)
    });
    
    if (respuesta.ok) {
      // Marcar como sincronizadas
      const notasActualizadas = notas.map(nota => {
        if (!nota.sincronizado) {
          return {...nota, sincronizado: true};
        }
        return nota;
      });
      
      localStorage.setItem('notasOffline', JSON.stringify(notasActualizadas));
      return {estado: 'completado', mensaje: `${notasPendientes.length} notas sincronizadas`};
    } else {
      throw new Error('Error en la respuesta del servidor');
    }
  } catch (error) {
    console.error('Error al sincronizar:', error);
    return {estado: 'error', mensaje: 'Error al sincronizar con el servidor'};
  }
}

Para sessionStorage:

  1. Estado de formularios por pasos:
// Guardar paso actual del formulario
function guardarPasoFormulario(paso, datos) {
  // Guardar el paso actual
  sessionStorage.setItem('formularioPasoActual', paso.toString());
  
  // Guardar los datos de este paso
  sessionStorage.setItem(`formularioPaso${paso}`, JSON.stringify(datos));
}

// Recuperar datos del paso anterior
function recuperarPasoAnterior() {
  const pasoActual = parseInt(sessionStorage.getItem('formularioPasoActual') || '1');
  if (pasoActual <= 1) return null;
  
  const pasoAnterior = pasoActual - 1;
  const datosJSON = sessionStorage.getItem(`formularioPaso${pasoAnterior}`);
  return datosJSON ? JSON.parse(datosJSON) : null;
}
  1. Datos de navegación de la sesión actual:
// Registrar página visitada
function registrarVisita(pagina) {
  // Obtener historial existente
  const historialJSON = sessionStorage.getItem('historialNavegacion');
  const historial = historialJSON ? JSON.parse(historialJSON) : [];
  
  // Añadir página actual con timestamp
  historial.push({
    pagina,
    tiempo: new Date().toISOString()
  });
  
  // Guardar historial actualizado
  sessionStorage.setItem('historialNavegacion', JSON.stringify(historial));
}

// Mostrar "migas de pan" basadas en la navegación
function generarMigasDePan() {
  const historialJSON = sessionStorage.getItem('historialNavegacion');
  if (!historialJSON) return [];
  
  const historial = JSON.parse(historialJSON);
  // Eliminar duplicados consecutivos
  const rutas = historial.filter((item, index, array) => {
    return index === 0 || item.pagina !== array[index - 1].pagina;
  });
  
  return rutas.map(item => item.pagina);
}
  1. Recuperación de datos en caso de recarga accidental:
// Guardar formulario automáticamente
function autoguardarFormulario() {
  const formulario = document.getElementById('miFormulario');
  const datos = new FormData(formulario);
  const objetoDatos = {};
  
  for (const [clave, valor] of datos.entries()) {
    objetoDatos[clave] = valor;
  }
  
  sessionStorage.setItem('formularioTemporal', JSON.stringify(objetoDatos));
  console.log('Formulario autoguardado:', new Date().toLocaleTimeString());
}

// Recuperar datos al cargar la página
function recuperarFormularioAutoguardado() {
  const datosJSON = sessionStorage.getItem('formularioTemporal');
  if (!datosJSON) return false;
  
  const datos = JSON.parse(datosJSON);
  const formulario = document.getElementById('miFormulario');
  
  // Rellenar campos del formulario
  for (const clave in datos) {
    const campo = formulario.elements[clave];
    if (campo) {
      campo.value = datos[clave];
    }
  }
  
  return true;
}

// Configurar autoguardado cada 30 segundos
setInterval(autoguardarFormulario, 30000);

// Al cargar la página
window.addEventListener('load', function() {
  if (recuperarFormularioAutoguardado()) {
    alert('Se ha recuperado el contenido del formulario anterior.');
  }
});

Seguridad y consideraciones

Al utilizar el almacenamiento web, debemos tener en cuenta varias consideraciones de seguridad:

  1. Datos sensibles: No almacenar información confidencial sin encriptación adicional, ya que el almacenamiento es accesible mediante JavaScript.
// Ejemplo de encriptación simple (NO usar en producción)
function guardarDatoSensible(clave, valor, secreto) {
  // Este es solo un ejemplo didáctico
  // En aplicaciones reales usar bibliotecas de encriptación adecuadas
  const valorCodificado = btoa(valor) + '_' + btoa(secreto);
  localStorage.setItem(clave, valorCodificado);
}

function recuperarDatoSensible(clave, secreto) {
  const valorCodificado = localStorage.getItem(clave);
  if (!valorCodificado) return null;
  
  const [valor, secretoAlmacenado] = valorCodificado.split('_');
  if (btoa(secreto) !== secretoAlmacenado) {
    return null; // Secreto incorrecto
  }
  
  return atob(valor);
}
  1. XSS: Un script malicioso podría acceder a los datos almacenados si logra ejecutarse en el contexto de la página.

  2. Almacenamiento compartido: En localStorage, todas las pestañas y ventanas del mismo origen comparten los datos, lo que podría llevar a condiciones de carrera.

  3. Actualizaciones: Al modificar la estructura de los datos almacenados en nuevas versiones de la aplicación, debemos manejar la migración correctamente.

// Ejemplo de manejo de versiones de datos almacenados
function inicializarAlmacenamiento() {
  // Comprobar versión actual
  const versionActual = localStorage.getItem('appVersion') || '1.0';
  const nuevaVersion = '2.0'; // Versión actual de la aplicación
  
  if (versionActual !== nuevaVersion) {
    console.log(`Actualizando almacenamiento de ${versionActual} a ${nuevaVersion}`);
    
    // Realizar migraciones según versión
    if (versionActual === '1.0') {
      // Migrar datos de formato v1.0 a v2.0
      const datosAntiguos = localStorage.getItem('preferencias');
      if (datosAntiguos) {
        try {
          const objAntiguo = JSON.parse(datosAntiguos);
          // Convertir formato antiguo a nuevo
          const objNuevo = {
            tema: objAntiguo.tema || 'claro',
            idioma: objAntiguo.idioma || 'es',
            // Nuevo campo en v2.0
            notificaciones: true
          };
          localStorage.setItem('preferenciasUsuario', JSON.stringify(objNuevo));
          // Eliminar clave antigua
          localStorage.removeItem('preferencias');
        } catch (e) {
          console.error('Error al migrar datos:', e);
        }
      }
    }
    
    // Actualizar versión
    localStorage.setItem('appVersion', nuevaVersion);
  }
}

Resumen

El almacenamiento web a través de localStorage y sessionStorage proporciona una forma eficiente y directa de almacenar datos en el navegador del usuario. Mientras que localStorage ofrece persistencia a largo plazo y compartición entre todas las páginas del mismo origen, sessionStorage es ideal para datos temporales específicos de una sesión o pestaña.

Ambas APIs comparten la misma interfaz simple de pares clave-valor, pero es importante recordar sus limitaciones: solo almacenan strings (requiriendo JSON.stringify() para objetos), tienen capacidad limitada, y no son adecuadas para datos sensibles sin protección adicional. A pesar de estas limitaciones, el almacenamiento web es una herramienta fundamental para mejorar la experiencia del usuario, optimizar el rendimiento y permitir funcionalidades offline en aplicaciones web modernas.