Estimar o espaço de armazenamento disponível

tl;dr

O Chrome 61, e mais navegadores no futuro, agora expõe uma estimativa da quantidade de armazenamento que um app da Web está usando e quanto está disponível por:

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

Apps da Web e armazenamento de dados modernos

Ao pensar nas necessidades de armazenamento de um aplicativo da Web moderno, é útil dividir o que está sendo armazenado em duas categorias: os dados principais necessários para carregar o aplicativo da Web e os dados necessários para uma interação significativa do usuário após o carregamento.

O primeiro tipo de dados, necessário para carregar seu app da Web, consiste em HTML, JavaScript, CSS e talvez algumas imagens. Os service workers, junto com a API Cache Storage, fornecem a infraestrutura necessária para salvar esses recursos principais e usá-los posteriormente para carregar rapidamente seu app da Web, de preferência ignorando completamente a rede. Ferramentas que se integram ao processo de build do seu app da Web, como as novas bibliotecas Workbox ou as sw-precache mais antigas, podem automatizar totalmente o processo de armazenamento, atualização e uso desse tipo de dados.

Mas e o outro tipo de dados? Esses são recursos que não são necessários para carregar seu app da Web, mas que podem desempenhar um papel crucial na experiência geral do usuário. Por exemplo, se você estiver criando um app da Web de edição de imagens, salve uma ou mais cópias locais de uma imagem, permitindo que os usuários alternem entre revisões e desfaçam o trabalho. Ou, se você estiver desenvolvendo uma experiência de reprodução de mídia off-line, salvar arquivos de áudio ou vídeo localmente será um recurso essencial. Todo app da Web que pode ser personalizado acaba precisando salvar algum tipo de informações de estado. Como você sabe quanto espaço está disponível para esse tipo de armazenamento no ambiente de execução e o que acontece quando fica sem espaço?

Passado: window.webkitStorageInfo e navigator.webkitTemporaryStorage

Historicamente, os navegadores oferecem suporte a esse tipo de introspecção por meio de interfaces prefixadas, como a muito antiga (e descontinuada) window.webkitStorageInfo e a navigator.webkitTemporaryStorage não tão antiga, mas ainda não padrão. Embora essas interfaces forneçam informações úteis, elas não têm um futuro como padrões da Web.

É aí que entra o padrão de armazenamento whatWG (link em inglês).

O futuro: navigator.storage

Como parte do trabalho contínuo no Storage Living Standard, algumas APIs úteis chegaram à interface StorageManager, que é exposta aos navegadores como navigator.storage. Como muitas outras APIs da Web mais recentes, navigator.storage está disponível apenas em origens seguras (exibidas via HTTPS ou localhost).

No ano passado, apresentamos o método navigator.storage.persist(), que permite que seu aplicativo da Web solicite que o armazenamento dele seja isento da limpeza automática.

Agora ele é unido pelo método navigator.storage.estimate(), que serve como uma substituição moderna para navigator.webkitTemporaryStorage.queryUsageAndQuota(). estimate() retorna informações semelhantes, mas expõe uma interface baseada em promessas (em inglês), que é compatível com outras APIs assíncronas modernas. A promessa que estimate() retorna é resolvida com um objeto que contém duas propriedades: usage, que representa o número de bytes usados no momento, e quota, que representa o máximo de bytes que podem ser armazenados pela origem atual. Assim como tudo o que está relacionado ao armazenamento, a cota é aplicada a uma origem inteira.

Se um aplicativo da Web tentar armazenar (por exemplo, o IndexedDB ou a API Cache Storage) dados grandes o suficiente para exceder a cota disponível em uma determinada origem, a solicitação falhará com uma exceção QuotaExceededError.

Estimativas de armazenamento em ação

A forma exata como você usa estimate() depende do tipo de dados que seu app precisa armazenar. Por exemplo, atualize um controle na interface que informe aos usuários quanto espaço está sendo usado após a conclusão de cada operação de armazenamento. O ideal é fornecer uma interface que permita aos usuários limpar manualmente os dados que não são mais necessários. Você pode escrever código assim:

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

Qual é a precisão da estimativa?

É difícil perder o fato de que os dados que você recebe da função são apenas uma estimativa do espaço que uma origem está usando. Está bem ali no nome da função! Os valores usage e quota não precisam ser estáveis. Portanto, é recomendável considerar o seguinte:

  • usage reflete quantos bytes uma determinada origem está usando efetivamente para dados da mesma origem, que, por sua vez, podem ser afetados por técnicas de compactação internas, blocos de alocação de tamanho fixo que podem incluir espaço não utilizado e a presença de registros"tombstone" que podem ser criados temporariamente após uma exclusão. Para evitar o vazamento de informações de tamanho exato, os recursos opacos de origem cruzada salvos localmente podem contribuir com mais bytes de padding para o valor usage geral.
  • quota reflete a quantidade de espaço atualmente reservado para uma origem. O valor depende de alguns fatores constantes, como o tamanho geral do armazenamento, mas também de vários fatores potencialmente voláteis, incluindo a quantidade de espaço de armazenamento não utilizado no momento. Assim, como outros aplicativos em um dispositivo gravam ou excluem dados, a quantidade de espaço que o navegador está disposto a dedicar à origem do seu app da Web provavelmente mudará.

O presente: detecção de recursos e substitutos

O estimate() está ativado por padrão a partir do Chrome 61. O Firefox está fazendo experimentos com navigator.storage, mas, desde agosto de 2017, ele não é ativado por padrão. É necessário ativar a preferência dom.storageManager.enabled para testá-la.

Ao trabalhar com funcionalidades que ainda não são compatíveis com todos os navegadores, a detecção de recursos é essencial. Você pode combinar a detecção de recursos com um wrapper baseado em promessa, além dos métodos navigator.webkitTemporaryStorage mais antigos para fornecer uma interface consistente com estas linhas de:

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