La cuota de almacenamiento y la política de desalojo
Cuánto puede guardar un origen, cómo se consulta el uso real desglosado, qué es el almacenamiento persistente y qué pasa exactamente cuando el disco se llena.
Todos los mecanismos de almacenamiento de este nivel comparten una misma bolsa por origen, con un límite que no es fijo, que no se anuncia, y que al agotarse produce excepciones en sitios que casi nadie captura. El panel muestra el uso desglosado por tipo, y una API permite consultarlo desde el código. Saber cuánto tienes, cuánto usas y qué pasa cuando se acaba es la diferencia entre una aplicación que degrada bien y una que se rompe en el dispositivo de alguien que tiene el disco lleno.
- Consultar la cuota y el uso desglosado por mecanismo de almacenamiento.
- Explicar de qué depende el límite y por qué no es un número fijo.
- Solicitar almacenamiento persistente y saber qué garantiza y qué no.
- Manejar el agotamiento de cuota sin romper la aplicación.
Cuánto hay disponible
La cuota de un origen no es una constante: se calcula como una fracción del espacio libre del disco, con un tope. Eso significa tres cosas que conviene interiorizar.
Varía entre dispositivos. Un portátil con medio terabyte libre da una cuota amplísima; un móvil con el almacenamiento casi lleno da una cuota minúscula. La misma aplicación, con el mismo código, tiene cuotas que difieren en órdenes de magnitud.
Varía en el tiempo, en el mismo dispositivo. Si el usuario llena el disco, tu cuota baja. Puede pasar de tener sitio de sobra a no tenerlo sin que nada haya cambiado en tu aplicación.
Es compartida entre todos los mecanismos. El almacén de respuestas, IndexedDB, el almacenamiento web y los registros de service workers cuentan contra el mismo total.
De ahí sale la regla operativa fundamental: nunca supongas un límite; consúltalo. Y consúltalo antes de escribir algo grande, no después de que falle.
// Cuota, uso y desglose por mecanismo
(async () => {
if (!navigator.storage?.estimate) return console.warn('API de estimacion no disponible.');
const e = await navigator.storage.estimate();
const mb = b => +(b / 1048576).toFixed(2);
console.table([{
usadoMB: mb(e.usage),
cuotaMB: mb(e.quota),
porcentaje: +(100 * e.usage / e.quota).toFixed(2) + '%',
libreMB: mb(e.quota - e.usage)
}]);
// El desglose por mecanismo no es estandar pero Chrome lo expone
if (e.usageDetails) {
console.table(Object.entries(e.usageDetails)
.map(([mecanismo, bytes]) => ({ mecanismo, mb: mb(bytes) }))
.sort((a, b) => b.mb - a.mb));
} else {
console.log('Este navegador no desglosa el uso por mecanismo.');
}
const persistente = await navigator.storage.persisted?.();
console.log('Almacenamiento persistente concedido:', persistente);
})();
El desglose por mecanismo es la parte más útil y no está estandarizada: cuando está disponible, responde directamente a la pregunta de qué está ocupando el espacio, y evita la investigación entera.
La cuota que devuelve la estimación está deliberadamente redondeada y puede estar por debajo del máximo real. Los navegadores no exponen la cifra exacta porque sería un vector de identificación de dispositivos. Sirve perfectamente para decidir si algo cabe y no para calcular al byte.
El desalojo
Cuando el espacio del dispositivo escasea, el navegador libera. El comportamiento tiene dos modos según el estado del origen.
Almacenamiento no persistente, que es el estado por defecto. El origen es candidato a desalojo. El navegador elimina todos los datos de un origen a la vez, no algunas entradas: no hay borrado parcial. El criterio de selección favorece a los orígenes menos usados recientemente. Además, algunos navegadores aplican un desalojo por inactividad prolongada.
Almacenamiento persistente. El origen queda exento del desalojo automático. Solo el usuario puede borrarlo, explícitamente.
El paso de un modo a otro se solicita, y la concesión depende del navegador:
// Solicitar almacenamiento persistente y comprobar el resultado
(async () => {
if (!navigator.storage?.persist) return console.warn('No disponible.');
const yaEra = await navigator.storage.persisted();
if (yaEra) return console.log('Ya era persistente.');
const concedido = await navigator.storage.persist();
console.log(concedido
? 'Concedido: este origen queda exento del desalojo automatico.'
: 'Denegado: el navegador no considera que este sitio lo merezca todavia.');
// La decision depende de heuristicas del navegador. Las habituales:
console.log('Los criterios tipicos que suben las probabilidades son:');
console.log(' - que el sitio este instalado como aplicacion');
console.log(' - que el usuario lo visite con frecuencia');
console.log(' - que le haya concedido permisos como notificaciones');
console.log(' - que lo haya marcado como favorito');
})();
Dos advertencias sobre esto. La primera: no lo pidas al arrancar. Pedirlo antes de que el usuario haya hecho nada suele resultar en denegación, y en algunos navegadores la petición puede mostrar un diálogo. El momento correcto es cuando el usuario hace algo que implica compromiso: guardar contenido para uso sin red, instalar la aplicación, crear una cuenta.
La segunda: la concesión no es una garantía absoluta. El usuario puede borrar los datos cuando quiera desde la configuración del navegador, y un sistema operativo bajo presión extrema de almacenamiento puede tomar sus propias medidas.
Manejar el agotamiento
Cuando la cuota se agota, cada mecanismo falla a su manera y en todos los casos hay que capturarlo.
El almacenamiento web lanza una excepción de cuota en la escritura. IndexedDB aborta la transacción con un error de cuota. La escritura en el almacén de respuestas rechaza la promesa. En los tres casos el error tiene un nombre reconocible.
// Escritura defensiva con comprobacion previa y recuperacion
async function guardarEnCacheConEspacio(nombreCache, peticion, respuesta, margenMB = 50) {
const est = await navigator.storage?.estimate?.();
if (est) {
const libreMB = (est.quota - est.usage) / 1048576;
if (libreMB < margenMB) {
console.warn('Poco espacio:', libreMB.toFixed(1), 'MB. Liberando antes de escribir.');
await liberarEspacio(nombreCache, 0.3); // suelta el 30% mas antiguo
}
}
try {
const cache = await caches.open(nombreCache);
await cache.put(peticion, respuesta.clone());
return true;
} catch (e) {
if (e.name === 'QuotaExceededError') {
console.warn('Cuota agotada. Liberando y reintentando una vez.');
await liberarEspacio(nombreCache, 0.5);
try {
const cache = await caches.open(nombreCache);
await cache.put(peticion, respuesta.clone());
return true;
} catch {
console.error('Sigue sin caber. Se continua sin cachear.');
return false; // la aplicacion sigue funcionando, solo sin caché
}
}
throw e;
}
}
async function liberarEspacio(nombreCache, fraccion) {
const cache = await caches.open(nombreCache);
const claves = await cache.keys();
const cuantas = Math.ceil(claves.length * fraccion);
await Promise.all(claves.slice(0, cuantas).map(k => cache.delete(k)));
console.log('Eliminadas', cuantas, 'entradas antiguas de', nombreCache);
}
La línea que importa de todo ese código es la del último return false: quedarse sin espacio para cachear no debe impedir que la aplicación funcione. La caché es una optimización; si no cabe, se sigue sin ella. Ese es el comportamiento correcto y es exactamente el que falta en la mayoría de las implementaciones, donde una excepción de cuota no capturada tumba el flujo entero.
La comprobación que hay que hacer una vez por proyecto
Tres pruebas, media hora, y encuentran problemas que ningún usuario reportará de forma comprensible.
Con el almacenamiento vacío. Borra todos los datos del origen desde el panel y usa la aplicación cinco minutos. Todo lo que se rompa es un bug real que le pasará a cualquier usuario nuevo o a cualquiera que haya sido desalojado.
Con la cuota agotada. Llena el almacenamiento hasta que falle y observa qué hace la aplicación. El fragmento siguiente lo consigue en segundos y es fácil de deshacer.
// Llena la cuota para probar el comportamiento ante agotamiento
(async () => {
const cache = await caches.open('prueba-de-cuota');
const bloque = new Uint8Array(5 * 1024 * 1024); // 5 MB por entrada
let i = 0;
try {
while (i < 2000) {
await cache.put('/relleno/' + i, new Response(bloque));
i++;
if (i % 20 === 0) {
const e = await navigator.storage.estimate();
console.log(i, 'bloques |', (e.usage / 1048576).toFixed(0), 'de',
(e.quota / 1048576).toFixed(0), 'MB');
}
}
} catch (e) {
console.warn('Cuota agotada tras', i, 'bloques. Error:', e.name);
}
console.log('Ahora usa la aplicacion y observa que se rompe.');
window.limpiarPruebaDeCuota = () => caches.delete('prueba-de-cuota')
.then(() => console.log('Relleno eliminado.'));
})();
Con datos corruptos. Edita a mano un valor guardado para que tenga una forma que la aplicación no espera, y comprueba si el fallo es recuperable.
Al cerrar este nivel merece la pena señalar la propiedad que unifica todo lo que contiene y que explica por qué el panel de aplicación resuelve una clase de bugs tan distinta de los demás paneles. Todo lo que hay aquí sobrevive a la recarga, sobrevive al despliegue y sobrevive a la versión del código que lo escribió. Es la única parte del sistema con esa característica: el DOM se reconstruye en cada carga, el estado en memoria desaparece, el código se sustituye entero al desplegar. El almacenamiento no. Eso tiene tres consecuencias que conviene tener presentes cada vez que aparece un bug raro. La primera, de diagnóstico: cuando algo falla para una persona y no para otra con el mismo código y el mismo navegador, la diferencia está casi siempre aquí. Es el primer sitio donde mirar ante un “a mí no me pasa”, y la ventana de incógnito es el desempate de cinco segundos que confirma o descarta la hipótesis entera. La segunda, de diseño: cada dato que decides persistir es una obligación de compatibilidad hacia atrás que aceptas para siempre. No hay forma de desplegar una migración a los navegadores de tus usuarios; solo puedes escribir código que sepa leer todas las formas que ese dato ha tenido en la historia del producto, o que sepa descartarlo con elegancia cuando no lo reconoce. Cuanto más guardes, más grande es esa superficie, y crece de forma monótona porque nadie borra nunca los formatos antiguos. La tercera, y la más operativa: necesitas un botón de pánico. Toda aplicación que persista estado debería tener una forma de que el usuario, o el soporte, lo borre todo y empiece de cero, sin tener que explicarle a nadie cómo se abre el panel de aplicación de un navegador. Una ruta oculta que vacíe los almacenes, se dé de baja de los service workers y recargue cuesta veinte líneas y convierte una categoría entera de incidentes irresolubles en una instrucción de una frase. Es una de esas piezas que parecen innecesarias hasta el día en que un cliente importante está bloqueado por un dato corrupto en su disco y no hay ninguna forma de arreglarlo desde el servidor.