Verfügbaren Speicherplatz schätzen

Kurzfassung

In Chrome 61 und in weiteren Browsern, die folgen werden, wird jetzt eine Schätzung angezeigt, wie viel Speicherplatz eine Webanwendung belegt und wie viel noch verfügbar ist:

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

Moderne Webanwendungen und Datenspeicher

Wenn Sie über die Speicheranforderungen einer modernen Webanwendung nachdenken, ist es hilfreich, die zu speichernden Daten in zwei Kategorien zu unterteilen: die Kerndaten, die zum Laden der Webanwendung erforderlich sind, und die Daten, die für eine sinnvolle Nutzerinteraktion nach dem Laden der Anwendung erforderlich sind.

Der erste Datentyp, der zum Laden Ihrer Webanwendung erforderlich ist, besteht aus HTML, JavaScript, CSS und möglicherweise einigen Bildern. Dienstworker und die Cache Storage API bieten die erforderliche Infrastruktur, um diese Hauptressourcen zu speichern und später zum schnellen Laden Ihrer Webanwendung zu verwenden, idealerweise ohne das Netzwerk zu nutzen. Mit Tools, die in den Build-Prozess Ihrer Webanwendung eingebunden sind, wie den neuen Workbox-Bibliotheken oder der älteren sw-precache, können Sie das Speichern, Aktualisieren und Verwenden dieser Art von Daten vollständig automatisieren.

Aber was ist mit den anderen Datentypen? Diese Ressourcen sind zwar nicht zum Laden Ihrer Webanwendung erforderlich, können aber eine entscheidende Rolle für die Nutzerfreundlichkeit spielen. Wenn Sie beispielsweise eine Web-App zur Bildbearbeitung entwickeln, sollten Sie eine oder mehrere lokale Kopien eines Bildes speichern, damit Nutzer zwischen den Versionen wechseln und ihre Arbeit rückgängig machen können. Wenn Sie beispielsweise eine Offline-Medienwiedergabe entwickeln, ist das lokale Speichern von Audio- oder Videodateien eine wichtige Funktion. Jede Web-App, die personalisiert werden kann, muss eine Art Statusinformationen speichern. Woher wissen Sie, wie viel Speicherplatz für diese Art von Laufzeitspeicher verfügbar ist, und was passiert, wenn der Speicherplatz aufgebraucht ist?

Bisher: window.webkitStorageInfo und navigator.webkitTemporaryStorage

Browser haben diese Art der Inspektion bisher über Präfix-Schnittstellen unterstützt, z. B. die sehr alte (und nicht mehr unterstützte) window.webkitStorageInfo und die nicht ganz so alte, aber immer noch nicht standardmäßige navigator.webkitTemporaryStorage. Diese Oberflächen lieferten zwar nützliche Informationen, haben aber keine Zukunft als Webstandards.

Hier kommt der WHATWG-Speicherstandard ins Spiel.

Die Zukunft: navigator.storage

Im Rahmen der laufenden Arbeiten am Storage Living Standard haben einige nützliche APIs den Weg in die StorageManager-Benutzeroberfläche gefunden, die für Browser als navigator.storage verfügbar ist. Wie viele andere neuere Web-APIs ist navigator.storage nur über sichere Ursprünge verfügbar (über HTTPS oder localhost bereitgestellt).

Letztes Jahr haben wir die Methode navigator.storage.persist() eingeführt, mit der Ihre Webanwendung anfordern kann, dass ihr Speicherplatz von der automatischen Bereinigung ausgenommen wird.

Die Methode navigator.storage.estimate() ist jetzt ein moderner Ersatz für navigator.webkitTemporaryStorage.queryUsageAndQuota(). estimate() gibt ähnliche Informationen zurück, bietet aber eine versprechensbasierte Schnittstelle, die mit anderen modernen asynchronen APIs übereinstimmt. Das von estimate() zurückgegebene Versprechen wird mit einem Objekt mit zwei Eigenschaften aufgelöst: usage, die Anzahl der derzeit verwendeten Bytes, und quota, die maximale Anzahl von Bytes, die vom aktuellen Ursprung gespeichert werden können. Wie alle anderen Speicherkontingente gilt das Kontingent für den gesamten Ursprung.

Wenn eine Webanwendung versucht, mithilfe von IndexedDB oder der Cache Storage API Daten zu speichern, die so groß sind, dass ein bestimmter Ursprung sein verfügbares Kontingent überschreitet, schlägt die Anfrage mit einer QuotaExceededError-Ausnahme fehl.

Speicherplatzschätzungen in der Praxis

Wie genau Sie estimate() verwenden, hängt davon ab, welche Daten Ihre App speichern muss. Sie könnten beispielsweise ein Steuerelement in Ihrer Benutzeroberfläche aktualisieren, damit Nutzer sehen, wie viel Speicherplatz nach jedem Speichervorgang belegt ist. Idealerweise stellen Sie dann eine Benutzeroberfläche bereit, mit der Nutzer nicht mehr benötigte Daten manuell bereinigen können. Sie könnten Code in etwa so schreiben:

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

Wie genau ist die Schätzung?

Es ist nicht zu übersehen, dass die Daten, die Sie von der Funktion zurückerhalten, nur eine Schätzung des Speicherplatzes sind, den ein Ursprung belegt. Das steht direkt im Funktionsnamen. Weder die usage- noch die quota-Werte sind als stabil vorgesehen. Daher sollten Sie Folgendes berücksichtigen:

  • usage gibt an, wie viele Byte ein bestimmter Ursprung tatsächlich für Daten desselben Ursprungs verwendet. Dies kann wiederum von internen Komprimierungstechniken, Allokationsblöcken mit fester Größe, die nicht genutzten Speicherplatz enthalten können, und der Anwesenheit von Tombstone-Einträgen beeinflusst werden, die nach dem Löschen vorübergehend erstellt werden können. Um das Auslaufen von Informationen zur genauen Größe zu verhindern, können lokal gespeicherte opaque Ressourcen, die über mehrere Ursprünge hinweg genutzt werden, zusätzliche Padding-Byte zum Gesamtwert von usage beitragen.
  • quota gibt den derzeit für einen Ursprung reservierten Speicherplatz an. Der Wert hängt von einigen konstanten Faktoren wie der Gesamtspeichergröße, aber auch von einer Reihe potenziell volatiler Faktoren ab, einschließlich der Menge an Speicherplatz, der derzeit nicht verwendet wird. Wenn also andere Anwendungen auf einem Gerät Daten schreiben oder löschen, ändert sich wahrscheinlich die Menge an Speicherplatz, die der Browser dem Ursprung Ihrer Webanwendung zuweisen kann.

Die Gegenwart: Feature-Erkennung und Fallbacks

estimate() ist ab Chrome 61 standardmäßig aktiviert. In Firefox wird navigator.storage derzeit getestet, ist aber seit August 2017 nicht mehr standardmäßig aktiviert. Sie müssen die dom.storageManager.enabled-Einstellung aktivieren, um sie zu testen.

Wenn Sie mit Funktionen arbeiten, die noch nicht in allen Browsern unterstützt werden, ist die Funktionserkennung unerlässlich. Sie können die Feature-Erkennung mit einem versprechensbasierten Wrapper über den älteren navigator.webkitTemporaryStorage-Methoden kombinieren, um eine einheitliche Benutzeroberfläche bereitzustellen, z. B.:

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