Estimar o espaço de armazenamento disponível

Texto longo, leia o resumo

O Chrome 61, com mais navegadores a seguir, agora mostra uma estimativa de quanto de armazenamento um app da Web está usando e quanto está disponível por meio de:

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 modernos e armazenamento de dados

Quando você pensa 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 depois que o aplicativo é carregado.

O primeiro tipo de dados, o que é 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 mais tarde para carregar rapidamente seu app da Web, idealmente ignorando a rede por completo. Ferramentas que se integram ao processo de build do seu app da Web, como as novas bibliotecas Workbox ou as mais antigas sw-precache, podem automatizar totalmente o processo de armazenamento, atualização e uso desse tipo de dados.

Mas e os outros tipos de dados? Esses são recursos que não são necessários para carregar seu app da Web, mas que podem ter um papel crucial na experiência geral do usuário. Se você estiver escrevendo um app da Web para edição de imagens, por exemplo, talvez seja necessário salvar 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 seria um recurso essencial. Todo app da Web que pode ser personalizado acaba precisando salvar algum tipo de informações de estado. Como saber quanto espaço está disponível para esse tipo de armazenamento de execução e o que acontece quando você fica sem espaço?

O passado: window.webkitStorageInfo e navigator.webkitTemporaryStorage

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

É aí que o padrão de armazenamento WHATWG entra em cena.

O futuro: navigator.storage

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

No ano passado, lançamos o método navigator.storage.persist(), que permite que seu app da Web solicite que o armazenamento seja excluído da limpeza automática.

Agora, ele é acompanhado 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, 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 número máximo de bytes que podem ser armazenados pela origem atual. Como tudo relacionado ao armazenamento, a cota é aplicada em toda a origem.

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

Estimativas de armazenamento em ação

A forma exata de usar estimate() depende do tipo de dados que o app precisa armazenar. Por exemplo, é possível atualizar um controle na interface para informar 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 um código como este:

// 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 não perceber que os dados que você recebe da função são apenas uma estimativa do espaço que uma origem está usando. Está no nome da função. Nem os valores usage nem quota são estáveis. Portanto, é recomendável considerar o seguinte:

  • usage reflete quantos bytes uma determinada origem está usando efetivamente para dados de mesma origem, que, por sua vez, podem ser afetados por técnicas de compactação interna, 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, recursos opacos de origem cruzada salvos localmente podem contribuir com bytes de preenchimento adicionais para o valor usage geral.
  • quota reflete a quantidade de espaço reservada atualmente 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 que não está em uso no momento. Portanto, à medida que outros aplicativos em um dispositivo gravam ou excluem dados, a quantidade de espaço que o navegador está disposto a dedicar à origem do app da Web provavelmente vai mudar.

O presente: detecção de recursos e substitutos

estimate() está ativado por padrão a partir do Chrome 61. O Firefox está testando o 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 têm suporte em todos os navegadores, a detecção de recursos é obrigatória. É possível combinar a detecção de recursos com um wrapper baseado em promessas sobre os métodos navigator.webkitTemporaryStorage mais antigos para fornecer uma interface consistente, como:

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