Cómo estimar el espacio de almacenamiento disponible

tl;dr

Chrome 61, que se agregarán más navegadores, ahora expone una estimación de la cantidad de almacenamiento que usa una app 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.`);
  });
}

Aplicaciones web modernas y almacenamiento de datos

Cuando piensas 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 la interacción significativa del usuario una vez que se carga la aplicación.

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

Pero ¿qué ocurre con el otro tipo de datos? Se trata de recursos que no son necesarios para cargar tu aplicación web, pero que pueden desempeñar un papel crucial en la experiencia del usuario general. Si escribes una aplicación web de edición de imágenes, por ejemplo, es posible que quieras guardar una o más copias locales de una imagen, lo que permite a los usuarios cambiar entre revisiones y deshacer su trabajo. O, 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 de tiempo de ejecución y qué sucede cuando te quedas sin espacio?

El pasado: window.webkitStorageInfo y navigator.webkitTemporaryStorage

Históricamente, los navegadores admitían este tipo de introspección mediante interfaces con prefijos, como window.webkitStorageInfo, que es muy antiguo (y obsoleto), y navigator.webkitTemporaryStorage, que no es tan antiguo, pero aún no es estándar. Si bien estas interfaces brindaron información útil, no tienen un futuro como los estándares web.

En este punto, 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 entrega 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 se exima su almacenamiento 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 coincide 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 un origen).

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

Estimaciones de almacenamiento en acción

La forma exacta en la que uses estimate() depende del tipo de datos que tu app necesite almacenar. Por ejemplo, puedes actualizar un control en tu interfaz para que los usuarios sepan cuánto espacio se está usando una vez que se completa cada operación de almacenamiento. Idealmente, deberías proporcionar una interfaz que les permita a los usuarios limpiar de forma manual los datos que ya no son necesarios. Puedes escribir código como en las siguientes líneas:

// 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 precisa 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 allí, en el nombre de la función! No se pretende que los valores usage ni quota sean estables, por lo que te recomendamos que tengas en cuenta lo siguiente:

  • usage refleja cuántos bytes usa un origen determinado de forma efectiva para datos de mismo origen, que, a su vez, pueden verse afectados por técnicas de compresión internas, bloques de asignación de tamaño fijo que pueden incluir espacio sin usar y la presencia de registros"tombstone" que pueden crearse de forma temporal después de una eliminación. Para evitar la filtración de información de tamaño exacto, los recursos opacos y de origen cruzado que se guardan de forma local pueden contribuir con bytes de padding adicionales al valor usage general.
  • quota refleja la cantidad de espacio que se reserva actualmente para un origen. El valor depende de algunos factores constantes, como el tamaño general del almacenamiento, pero también de una serie de factores potencialmente volátiles, incluida la cantidad de espacio de almacenamiento que no se usa en este momento. Por lo tanto, a medida que 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 como la siguiente:

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});
}