Stima dello spazio di archiviazione disponibile

Jacopo Posnick
Jacopo Posnick

tl;dr

Ora Chrome 61, con altri browser da seguire, mostra una stima dello spazio di archiviazione utilizzato 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.`);
  });
}

Archiviazione dei dati e app web moderne

Pensando alle esigenze di archiviazione di un'applicazione web moderna, può essere 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 significativa dell'utente dopo il caricamento dell'applicazione.

Il primo tipo di dati, necessari per caricare l'app web, è costituito da HTML, JavaScript, CSS e magari alcune immagini. I service worker, insieme all'API Cache Storage, forniscono l'infrastruttura necessaria per salvare le risorse di base e utilizzarle in un secondo momento per caricare rapidamente la tua app web, evitando idealmente la rete. Gli strumenti che si integrano con il processo di compilazione dell'app web, come le nuove librerie Workbox o la versione precedente 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 che non sono necessarie per caricare la tua app web, ma che potrebbero avere un ruolo cruciale nella tua esperienza utente complessiva. Ad esempio, se stai scrivendo un'app web per la modifica di immagini, potresti voler salvare una o più copie locali di un'immagine, in modo che gli utenti possano passare da una revisione all'altra e annullare il lavoro. Oppure, se stai sviluppando un'esperienza di riproduzione di contenuti multimediali offline, il salvataggio di file audio o video localmente sarebbe una funzionalità fondamentale. Ogni app web personalizzabile deve salvare alcune informazioni sullo stato. Come fai a sapere quanto spazio è disponibile per questo tipo di archiviazione di runtime e cosa succede quando esaurisci lo spazio disponibile?

Passato: window.webkitStorageInfo e navigator.webkitTemporaryStorage

Storicamente i browser supportano questo tipo di introspezione tramite interfacce con prefisso, come la vecchia versione (e deprecata) window.webkitStorageInfo e la versione non precedente, ma ancora non standard, navigator.webkitTemporaryStorage. Queste interfacce forniscono informazioni utili, ma non hanno un futuro come standard web.

Ed è qui che entra in gioco lo standard WHATWG Storage Standard.

Il futuro: navigator.storage

Nell'ambito del lavoro continuo su Storage Living Standard, un paio di API utili sono state implementate con l'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 l'esenzione dalla pulizia automatica dello spazio di archiviazione.

Ora viene aggiunto il metodo navigator.storage.estimate(), che sostituisce moderno il criterio navigator.webkitTemporaryStorage.queryUsageAndQuota(). estimate() restituisce informazioni simili, ma espone un'interfaccia basata su promise, 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 tutto il resto relativo allo 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 abbastanza grandi da far superare una determinata origine alla quota disponibile, la richiesta avrà esito negativo con un'eccezione QuotaExceededError.

Stime dello 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 che consenta agli utenti di sapere quanto spazio viene utilizzato dopo il completamento di ogni operazione di archiviazione. L'ideale sarebbe fornire un'interfaccia che consenta agli utenti di ripulire manualmente i dati non più necessari. Potresti scrivere codice simile a quanto segue:

// 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 che i dati recuperati dalla funzione sono solo una stima dello spazio utilizzato da un'origine. È proprio lì, nel nome della funzione. Né i valori usage né i valori quota sono destinati a essere stabili, ti consigliamo di tenere presente quanto segue:

  • usage riflette il numero di byte utilizzati in modo efficace da una determinata origine per i dati stessa origine, che a loro volta possono essere influenzati da tecniche di compressione interne, 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 fuga di informazioni esatte sulle dimensioni, le risorse opache e multiorigine salvate localmente potrebbero contribuire ad aggiungere byte di spaziatura interna al valore complessivo di usage.
  • quota riflette la quantità di spazio attualmente riservata per un'origine. Il valore dipende da alcuni fattori costanti come la dimensione complessiva dello spazio di archiviazione, ma anche da una serie di fattori potenzialmente volatili, tra cui la quantità di spazio di archiviazione attualmente inutilizzato. Pertanto, come altre applicazioni su un dispositivo scrivono o eliminano dati, la quantità di spazio che il browser è disposta a dedicare all'origine della tua applicazione web probabilmente cambierà.

Il presente: rilevamento delle funzionalità e fallback

L'app estimate() è attiva per impostazione predefinita a partire da Chrome 61. Firefox sta sperimentando navigator.storage, ma da agosto 2017 non è attivo per impostazione predefinita. Devi attivare la preferenza dom.storageManager.enabled per testarla.

Quando lavori con funzionalità non ancora supportate in tutti i browser, il rilevamento delle funzionalità è un must. Puoi combinare il rilevamento delle funzionalità con un wrapper basato sulle promesse in aggiunta ai metodi navigator.webkitTemporaryStorage precedenti per fornire un'interfaccia coerente sulla linea di:

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