Stima dello spazio di archiviazione disponibile

tl;dr

Chrome 61, a cui seguiranno altri browser, ora mostra una stima della quantità di spazio di archiviazione utilizzata da un'app web e di quanto è disponibile tramite:

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

App web moderne e archiviazione dati

Quando si considerano le esigenze di archiviazione di una moderna applicazione web, è utile suddividere i dati archiviati in due categorie: i dati di base necessari per caricare l'applicazione web e i dati necessari per un'interazione utente significativa dopo il caricamento dell'applicazione.

Il primo tipo di dati, necessario per caricare la tua applicazione web, è costituito da HTML, JavaScript, CSS e forse alcune immagini. I service worker, insieme all'API Cache Storage, forniscono l'infrastruttura necessaria per salvare queste risorse principali e quindi utilizzarle in un secondo momento per caricare rapidamente la tua app web, bypassando completamente la rete. Gli strumenti che si integrano con il processo di creazione della tua app web, come le nuove librerie Workbox o le versioni precedenti sw-precache, sono in grado di automatizzare completamente il processo di archiviazione, aggiornamento e utilizzo di questo tipo di dati.

E per quanto riguarda l'altro tipo di dati? Si tratta di risorse che non sono necessarie per caricare la tua app web, ma che potrebbero svolgere un ruolo cruciale nell'esperienza utente complessiva. Se stai scrivendo un'app web per la modifica delle immagini, ad esempio, potresti salvare una o più copie locali di un'immagine, consentendo agli utenti di passare da una revisione all'altra e annullare il proprio lavoro. Oppure, se stai sviluppando un'esperienza di riproduzione multimediale offline, il salvataggio locale dei file audio o video sarebbe fondamentale. Ogni app web che può essere personalizzata deve salvare alcune informazioni sullo stato. Come fai a sapere quanto spazio è disponibile per questo tipo di spazio di archiviazione del runtime e cosa succede quando lo esaurisci?

Il passato: window.webkitStorageInfo e navigator.webkitTemporaryStorage

Storicamente i browser supportavano questo tipo di introspezione tramite interfacce con prefisso, come la vecchia (e deprecata) window.webkitStorageInfo e navigator.webkitTemporaryStorage ancora non standard ma non standard. Sebbene queste interfacce abbiano fornito informazioni utili, non hanno un futuro come standard web.

È qui che entra in gioco WHATWG Storage Standard.

Il futuro: navigator.storage

Nell'ambito del lavoro in corso su Storage Living Standard, sono state aggiunte un paio di utili API all'interfaccia di StorageManager, esposta ai browser come navigator.storage. Come molte altre API web più recenti, navigator.storage è disponibile solo su origini sicure (pubblicate tramite HTTPS o localhost).

L'anno scorso, abbiamo introdotto il metodo navigator.storage.persist(), che consente alla tua applicazione web di richiedere l'esenzione del proprio spazio di archiviazione dalla pulizia automatica.

Ora è affiancato dal metodo navigator.storage.estimate(), che sostituisce moderno navigator.webkitTemporaryStorage.queryUsageAndQuota(). estimate() restituisce informazioni simili, ma espone un'interfaccia basata su promessa, in linea con le altre API asincrone moderne. La promessa che estimate() restituisce si risolve con un oggetto contenente due proprietà: usage, che rappresenta il numero di byte attualmente utilizzati, e quota, che rappresenta il numero massimo di byte che possono essere archiviati dall'origine attuale. (Come per tutto ciò che riguarda lo spazio di archiviazione, la quota viene applicata a un'intera origine).

Se un'applicazione web tenta di archiviare, ad esempio utilizzando IndexedDB o l'API Cache Storage, dati abbastanza grandi da portare una determinata origine oltre la quota disponibile, la richiesta avrà esito negativo con un'eccezione QuotaExceededError.

Stime sullo spazio di archiviazione in azione

Il modo esatto in cui utilizzi estimate() dipende dal tipo di dati che l'app deve archiviare. Ad esempio, puoi aggiornare un controllo nell'interfaccia per far sapere agli utenti quanto spazio viene utilizzato al termine di ogni operazione di archiviazione. Idealmente, quindi, dovresti fornire un'interfaccia che consenta agli utenti di eseguire manualmente la pulizia dei dati che non sono più necessari. Potresti scrivere il codice sulla base di:

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

Quanto è precisa la stima?

È difficile dimenticare il fatto che i dati che ottieni dalla funzione siano solo una stima dello spazio utilizzato da un'origine. È proprio lì nel nome della funzione. Né i valori usage né i valori quota sono pensati per essere stabili, quindi ti consigliamo di tenere conto di quanto segue:

  • usage riflette la quantità di byte utilizzata efficacemente da una determinata origine per i dati della stessa origine, che a loro volta possono essere influenzati da tecniche di compressione interna, blocchi di allocazione a dimensioni fisse che potrebbero includere spazio inutilizzato e la presenza di record "tombstone" che potrebbero essere creati temporaneamente dopo un'eliminazione. Per evitare la perdita di informazioni sulle dimensioni esatte, le risorse opache multiorigine salvate localmente possono contribuire a una spaziatura interna aggiuntiva di byte al valore usage complessivo.
  • quota riflette la quantità di spazio attualmente riservata per un'origine. Il valore dipende da alcuni fattori costanti, come le dimensioni complessive dello spazio di archiviazione, ma anche da una serie di fattori potenzialmente volatili, tra cui la quantità di spazio di archiviazione attualmente inutilizzato. Perciò, poiché altre applicazioni su un dispositivo scrivono o eliminano dati, la quantità di spazio che il browser è disposto a dedicare all'origine della tua app web probabilmente cambierà.

Presente: rilevamento delle funzionalità e fallback

estimate() viene attivato per impostazione predefinita a partire da Chrome 61. Firefox sta sperimentando navigator.storage, ma da agosto 2017 questa funzionalità non è attiva per impostazione predefinita. Devi attivare la preferenza dom.storageManager.enabled per testarla.

Quando si utilizzano funzionalità che non sono ancora supportate in tutti i browser, il rilevamento delle funzionalità è indispensabile. Puoi combinare il rilevamento delle funzionalità con un wrapper basato su promessa oltre ai metodi navigator.webkitTemporaryStorage precedente per fornire un'interfaccia coerente:

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