Stima dello spazio di archiviazione disponibile

tl;dr

Chrome 61, seguito da altri browser, ora mostra una stima dello spazio di archiviazione utilizzato da un'app web e di quello 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 dei dati

Quando pensi alle esigenze di archiviazione di un'applicazione web moderna, è utile suddividere ciò che viene archiviato in due categorie: i dati di base necessari per caricare l'applicazione web e i dati necessari per un'interazione significativa con l'utente una volta caricata l'applicazione.

Il primo tipo di dati, necessario per caricare l'app 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 di base e utilizzarle in un secondo momento per caricare rapidamente la tua app web, idealmente bypassando completamente la rete. Gli strumenti che si integrano con il processo di compilazione della tua app web, come le nuove librerie Workbox o le precedenti sw-precache, possono 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 non necessarie per caricare la tua app web, ma che potrebbero svolgere un ruolo fondamentale nell'esperienza complessiva dell'utente. Ad esempio, se stai scrivendo un'app web di modifica delle immagini, potresti voler salvare una o più copie locali di un'immagine, consentendo agli utenti di passare da una revisione all'altra e annullare il loro lavoro. In alternativa, se stai sviluppando un'esperienza di riproduzione di contenuti multimediali offline, il salvataggio locale dei file audio o video è una funzionalità fondamentale. Ogni web app che può essere personalizzata deve salvare una sorta di informazioni sullo stato. Come faccio a sapere quanto spazio è disponibile per questo tipo di spazio di archiviazione di runtime e cosa succede quando non ho più spazio?

In passato: window.webkitStorageInfo e navigator.webkitTemporaryStorage

I browser hanno sempre supportato questo tipo di introspezione tramite interfacce con prefisso, come la molto vecchia (e deprecata) window.webkitStorageInfo e la non proprio vecchia, ma ancora non standard navigator.webkitTemporaryStorage. Sebbene queste interfacce fornissero informazioni utili, non hanno un futuro come standard web.

È qui che entra in gioco lo standard di archiviazione WHATWG.

Il futuro: navigator.storage

Nell'ambito del lavoro in corso sullo Storage Living Standard, un paio di API utili sono state integrate nell'interfaccia 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 che lo spazio di archiviazione sia esente dalla pulizia automatica.

Ora è affiancato dal metodo navigator.storage.estimate(), che funge da sostituzione moderna di navigator.webkitTemporaryStorage.queryUsageAndQuota(). estimate() restituisce informazioni simili, ma espone un'interfaccia basata su promesse, in linea con altre API asincrone moderne. La promessa restituita da estimate() 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 corrente. 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, utilizzando ad esempio IndexedDB o l'API Cache Storage, dati sufficientemente grandi da far superare a una determinata origine la quota disponibile, la richiesta non andrà a buon fine con un'eccezione QuotaExceededError.

Stime dello spazio di archiviazione in azione

Il modo in cui utilizzi estimate() dipende dal tipo di dati che la tua app deve memorizzare. Ad esempio, potresti aggiornare un controllo nell'interfaccia per comunicare agli utenti quanto spazio viene utilizzato al termine di ogni operazione di archiviazione. Idealmente, dovresti fornire un'interfaccia che consenta agli utenti di ripulire manualmente i dati non più necessari. Potresti scrivere codice del tipo:

// 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 è accurata la stima?

È difficile non notare che i dati restituiti dalla funzione sono solo una stima dello spazio utilizzato da un'origine. È proprio lì nel nome della funzione. Né i valori usagequota sono destinati a essere stabili, pertanto è consigliabile tenere conto di quanto segue:

  • usage riflette il numero di byte che una determinata origine utilizza effettivamente per i dati di stessa origine, che a loro volta possono essere interessati da tecniche di compressione interna, blocchi di allocazione di dimensioni fisse che potrebbero includere spazio inutilizzato e la presenza di record "tombstone" che potrebbero essere creati temporaneamente a seguito di un'eliminazione. Per impedire la fuga di informazioni sulle dimensioni esatte, le risorse opache cross-origin salvate localmente possono contribuire con byte di riempimento aggiuntivi al valore complessivo di usage.
  • quota riflette la quantità di spazio attualmente prenotata 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, inclusa la quantità di spazio di archiviazione attualmente inutilizzato. Pertanto, man mano che altre applicazioni su un dispositivo scrivono o eliminano i dati, la quantità di spazio che il browser è disposto a dedicare all'origine della tua app web potrebbe cambiare.

Il presente: rilevamento delle funzionalità e soluzioni alternative

estimate() è attivata per impostazione predefinita a partire da Chrome 61. Firefox sta sperimentando navigator.storage, ma, da agosto 2017, non è attivato per impostazione predefinita. Per testarlo, devi attivare la preferenza dom.storageManager.enabled.

Quando si utilizzano funzionalità non ancora supportate in tutti i browser, il rilevamento delle funzionalità è un must. Puoi combinare il rilevamento delle funzionalità con un wrapper basato su promise sui metodi navigator.webkitTemporaryStorage precedenti per fornire un'interfaccia coerente, ad esempio:

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