Cómo estimar el espacio de almacenamiento disponible

tl;dr

Chrome 61, que se sumará a más navegadores, ahora expone una estimación de cuánto almacenamiento usa una aplicación web y cuánto está disponible a través de lo siguiente:

if ('storage' in navigator && 'estimate' in navigator.storage) {
  navigator.storage.estimate().then(({usage, quota}) => {
    console.log(`Using ${usage} out of ${quota} bytes.`);
  });
}

Apps web modernas y almacenamiento de datos

Cuando se piensa en las necesidades de almacenamiento de una aplicación web moderna, es útil dividir lo que se almacena en dos categorías: los datos principales necesarios para cargar la aplicación web y los datos necesarios para una interacción significativa del usuario una vez que se carga la aplicación.

El primer tipo de datos, los necesarios para cargar la app web, consisten en HTML, JavaScript, CSS y, tal vez, algunas imágenes. Los service workers, junto con la API de Cache Storage, proporcionan la infraestructura necesaria para guardar esos recursos principales y usarlos más tarde para cargar con rapidez la app web, idealmente omitiendo la red por completo. (Las herramientas que se integran en el proceso de compilación de la app web, como las bibliotecas nuevas de Workbox o la versión anterior de sw-precache, pueden automatizar por completo el proceso de almacenamiento, actualización y uso de este tipo de datos).

Pero ¿qué ocurre con los otros tipos de datos? Estos son recursos que no son necesarios para cargar la aplicación web, pero que pueden desempeñar un papel crucial en la experiencia general del usuario. Si escribes una app web de edición de imágenes, por ejemplo, es posible que quieras guardar una o más copias locales de una imagen para que los usuarios puedan cambiar entre revisiones y deshacer su trabajo. O bien, si estás desarrollando una experiencia de reproducción de contenido multimedia sin conexión, guardar archivos de audio o video de forma local sería una función fundamental. Cada app web que se puede personalizar termina necesitando guardar algún tipo de información de estado. ¿Cómo sabes cuánto espacio hay disponible para este tipo de almacenamiento en tiempo de ejecución y qué sucede cuando te quedas sin espacio?

Anteriores: window.webkitStorageInfo y navigator.webkitTemporaryStorage

Históricamente, los navegadores han admitido este tipo de introspección a través de interfaces con prefijos, como la window.webkitStorageInfo, que es muy antigua (y obsoleta), y la navigator.webkitTemporaryStorage, que no es tan antigua, pero aún no estándar. Si bien estas interfaces proporcionaron información útil, no tienen un futuro como los estándares web.

Aquí es donde entra en juego WHATWG Storage Standard.

El futuro: navigator.storage

Como parte del trabajo continuo en Storage Living Standard, un par de APIs útiles llegaron a la interfaz StorageManager, que se expone a los navegadores como navigator.storage. Al igual que muchas otras APIs web más recientes, navigator.storage solo está disponible en orígenes seguros (se entregan a través de HTTPS o localhost).

El año pasado, presentamos el método navigator.storage.persist(), que permite que tu aplicación web solicite que su almacenamiento quede exento de la limpieza automática.

Ahora, está unida por el método navigator.storage.estimate(), que sirve como reemplazo moderno de navigator.webkitTemporaryStorage.queryUsageAndQuota(). estimate() muestra información similar, pero expone una interfaz basada en promesas, que está en consonancia con otras APIs asíncronas modernas. La promesa que muestra estimate() se resuelve con un objeto que contiene dos propiedades: usage, que representa la cantidad de bytes que se usan actualmente, y quota, que representa la cantidad máxima de bytes que puede almacenar el origen actual. (Como todo lo relacionado con el almacenamiento, la cuota se aplica a todo el origen).

Si una aplicación web intenta almacenar (por ejemplo, IndexedDB o la API de Cache Storage) datos que son lo suficientemente grandes como para que un origen determinado supere su cuota disponible, la solicitud fallará y se generará una excepción de QuotaExceededError.

Estimaciones de almacenamiento en acción

La manera exacta en la que usas estimate() depende del tipo de datos que deba almacenar tu app. Por ejemplo, puedes actualizar un control en tu interfaz para que los usuarios sepan cuánto espacio se está usando después de que se completa cada operación de almacenamiento. Lo ideal sería que proporciones una interfaz que permita a los usuarios limpiar los datos que ya no sean necesarios de forma manual. Puedes escribir código como los siguientes ejemplos:

// For a primer on async/await, see
// https://developers.google.com/web/fundamentals/getting-started/primers/async-functions
async function storeDataAndUpdateUI(dataUrl) {
  // Pro-tip: The Cache Storage API is available outside of service workers!
  // See https://googlechrome.github.io/samples/service-worker/window-caches/
  const cache = await caches.open('data-cache');
  await cache.add(dataUrl);

  if ('storage' in navigator && 'estimate' in navigator.storage) {
    const {usage, quota} = await navigator.storage.estimate();
    const percentUsed = Math.round(usage / quota * 100);
    const usageInMib = Math.round(usage / (1024 * 1024));
    const quotaInMib = Math.round(quota / (1024 * 1024));

    const details = `${usageInMib} out of ${quotaInMib} MiB used (${percentUsed}%)`;

    // This assumes there's a <span id="storageEstimate"> or similar on the page.
    document.querySelector('#storageEstimate').innerText = details;
  }
}

¿Qué tan exacta es la estimación?

Es difícil pasar por alto el hecho de que los datos que obtienes de la función son solo una estimación del espacio que usa un origen. ¡Está justo en el nombre de la función! Ni los valores usage ni quota están diseñados para ser estables, por lo que se recomienda que tengas en cuenta lo siguiente:

  • usage refleja la cantidad de bytes que un origen determinado usa de manera efectiva para los datos del mismo origen, que, a su vez, pueden verse afectados por las técnicas de compresión interna, los bloques de asignación de tamaño fijo que pueden incluir espacio sin usar y la presencia de registros “tombstone” que se pueden crear de forma temporal después de una eliminación. Para evitar la filtración de información de tamaño exacto, el origen cruzado y los recursos opacos guardados de forma local pueden aportar bytes de padding adicionales al valor usage general.
  • quota refleja la cantidad de espacio reservado actualmente para un origen. El valor depende de algunos factores constantes, como el tamaño de almacenamiento general, pero también de un número de factores potencialmente volátiles, como la cantidad de espacio de almacenamiento que no se usa en la actualidad. Así, como otras aplicaciones de un dispositivo escriben o borran datos, es probable que cambie la cantidad de espacio que el navegador está dispuesto a dedicar al origen de tu app web.

El presente: detección de funciones y resguardos

estimate() está habilitado de forma predeterminada a partir de Chrome 61. Firefox está experimentando con navigator.storage, pero, desde agosto de 2017, no está activado de forma predeterminada. Debes habilitar la preferencia dom.storageManager.enabled para probarla.

Cuando se trabaja con funciones que aún no son compatibles con todos los navegadores, la detección de funciones es imprescindible. Puedes combinar la detección de funciones junto con un wrapper basado en promesas, además de los métodos navigator.webkitTemporaryStorage anteriores, para proporcionar una interfaz coherente en las siguientes líneas:

function storageEstimateWrapper() {
  if ('storage' in navigator && 'estimate' in navigator.storage) {
    // We've got the real thing! Return its response.
    return navigator.storage.estimate();
  }

  if ('webkitTemporaryStorage' in navigator &&
      'queryUsageAndQuota' in navigator.webkitTemporaryStorage) {
    // Return a promise-based wrapper that will follow the expected interface.
    return new Promise(function(resolve, reject) {
      navigator.webkitTemporaryStorage.queryUsageAndQuota(
        function(usage, quota) {resolve({usage: usage, quota: quota})},
        reject
      );
    });
  }

  // If we can't estimate the values, return a Promise that resolves with NaN.
  return Promise.resolve({usage: NaN, quota: NaN});
}