הערכת נפח האחסון הזמין

tl;dr

בגרסה 61 של Chrome, עם דפדפנים נוספים, יש עכשיו אומדן של נפח האחסון שאפליקציית אינטרנט משתמשת בו וכמות האחסון הזמינה דרך:

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

אפליקציות אינטרנט מודרניות ואחסון נתונים

כשמדברים על צורכי האחסון של אפליקציית אינטרנט מודרנית, כדאי לחלק את מה שמאוחסן לשתי קטגוריות: הנתונים העיקריים שדרושים לטעינת אפליקציית האינטרנט, והנתונים שדרושים לאינטראקציה משמעותית של המשתמש אחרי שהאפליקציה נטענת.

סוג הנתונים הראשון, שנדרש כדי לטעון את אפליקציית האינטרנט, מורכב מ-HTML, JavaScript, CSS ואולי גם כמה תמונות. באמצעות Service Worker ו-Cache Storage API תוכלו לספק את התשתית הדרושה לשמירת משאבי הליבה, ולהשתמש בהם מאוחר יותר כדי לטעון את אפליקציית האינטרנט שלכם במהירות. באופן אידיאלי, המערכת תעקוף לחלוטין את הרשת. (כלים שמשתלבים בתהליך ה-build של אפליקציית האינטרנט, כמו ספריות Workbox חדשות או sw-precache, יכולים להפוך לאוטומטיים את תהליך האחסון, העדכון והשימוש בנתונים מהסוג הזה.)

אבל מה לגבי סוג הנתונים השני? אלו משאבים שלא נחוצים כדי לטעון את אפליקציית האינטרנט, אבל אולי יש להם תפקיד קריטי בחוויית המשתמש הכוללת. לדוגמה, כשכותבים אפליקציית אינטרנט לעריכת תמונות, כדאי לשמור עותק מקומי אחד או יותר של תמונה, וכך המשתמשים יכולים לעבור בין גרסאות קודמות ולבטל את הפעולה שלהם. לחלופין, אם אתם מפתחים חוויית הפעלה של מדיה אופליין, שמירה של קובצי אודיו או וידאו באופן מקומי תהיה תכונה חשובה. כל אפליקציית אינטרנט שאפשר להתאים אישית תצטרך לשמור סוג מסוים של פרטי מצב. איך אפשר לדעת כמה מקום פנוי באחסון לזמן ריצה מהסוג הזה, ומה קורה כשנגמר המקום?

הנתונים הקודמים: window.webkitStorageInfo ו-navigator.webkitTemporaryStorage

בעבר דפדפנים תמכו בסוג הזה של בחינה עצמית באמצעות ממשקים עם תחילית, כמו הגרסה הישנה מאוד (וההוצאה משימוש) window.webkitStorageInfo, או הגרסה הישנה (navigator.webkitTemporaryStorage) שלא סטנדרטית. הממשקים האלה סיפקו מידע שימושי, אבל אין להם עתיד כסטנדרטים באינטרנט.

כאן נכנסת לתמונה whatWG Storage Standard.

העתיד: navigator.storage

במסגרת העבודה המתמשכת על Storage Living Standard, כמה ממשקי API שימושיים הפכו לממשק StorageManager, שחשוף לדפדפנים בתור navigator.storage. בדומה להרבה ממשקי API חדשים אחרים באינטרנט, navigator.storage זמין רק במקורות מאובטחים (מוצגים דרך HTTPS או Localhost).

בשנה שעברה השקנו את השיטה navigator.storage.persist(), שמאפשרת לאפליקציית האינטרנט לבקש שהאחסון שלה יהיה פטור מניקוי אוטומטי.

היא מחוברת עכשיו באמצעות ה-method navigator.storage.estimate(), שמשמשת כתחליף מודרני ל-navigator.webkitTemporaryStorage.queryUsageAndQuota(). הפקודה estimate() מחזירה מידע דומה, אבל חושפת ממשק מבוסס-הבטחה, שתואם לממשקי API אסינכרוניים מודרניים אחרים. ההבטחה שמחזירה estimate() מסתיימת באובייקט שמכיל שני מאפיינים: usage, שמייצג את מספר הבייטים בשימוש הנוכחי, ו-quota, שמייצג את המספר המקסימלי של הבייטים שאפשר לאחסן על ידי המקור הנוכחי. (כמו כל דבר אחר שקשור לאחסון, המכסה חלה על כל המקור).

אם אפליקציית אינטרנט מנסה לאחסן, למשל, IndexedDB או ממשק ה-API של Cache Storage, נתונים גדולים מספיק כדי שהמקור יחרוג מהמכסה הזמינה שלו, הבקשה תיכשל במקרה של חריגה QuotaExceededError.

אומדני נפח האחסון בפעולה

אופן השימוש המדויק ב-estimate() תלוי בסוג הנתונים שהאפליקציה צריכה לאחסן. לדוגמה, אפשר לעדכן אמצעי בקרה בממשק, שמיידע את המשתמשים כמה נפח אחסון נמצא בשימוש בסיום כל פעולת אחסון. במצב כזה כדאי לספק ממשק שמאפשר למשתמשים לנקות ידנית נתונים שכבר לא נחוצים. אפשר לכתוב קוד בשורות של:

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

עד כמה מדויקת ההערכה?

קשה לפספס את העובדה שהנתונים שמתקבלים מהפונקציה הם רק הערכה של השטח שבו מקור מסוים משתמש. הוא נמצא ממש בשם הפונקציה! הערכים usage וגם quota לא מיועדים להיות יציבים, לכן מומלץ להביא בחשבון את הנקודות הבאות:

  • usage משקף את מספר הבייטים שמשמשים בפועל את מקור נתון לנתונים מאותו מקור, שבתורו יכול להיות מושפע מטכניקות דחיסה פנימיות, בלוקים של הקצאה בגודל קבוע שעשויות לכלול שטח שלא נמצא בשימוש, ואת הנוכחות של רשומות 'Tombstone' שעלולות להיווצר באופן זמני אחרי מחיקה. כדי למנוע דליפה של פרטי גודל מדויקים, משאבים אטומים שנשמרים באופן מקומי ממקורות שונים עשויים להוסיף בייטים של מרווח פנימי לערך הכולל של usage.
  • quota משקפת את נפח האחסון ששמור כרגע למקור כלשהו. הערך תלוי בגורמים קבועים מסוימים, כמו גודל האחסון הכולל, אבל גם במספר גורמים שעשויים להיות תנודתיים, כולל נפח האחסון שלא נמצא כרגע בשימוש. כך שככל שאפליקציות אחרות במכשיר כותבות או מוחקות נתונים, כמות השטח שהדפדפן מוכן להקדיש למקור של אפליקציית האינטרנט כנראה ישתנה.

ההווה: זיהוי תכונות וחלופות

estimate() מופעל כברירת מחדל החל מ-Chrome 61. דפדפן Firefox פועל עם navigator.storage, אבל החל מאוגוסט 2017 הוא לא מופעל כברירת מחדל. צריך להפעיל את ההעדפה dom.storageManager.enabled כדי לבדוק אותה.

כשעובדים עם פונקציונליות שעדיין לא נתמכת בכל הדפדפנים, חובה להשתמש בזיהוי התכונות. אפשר לשלב את זיהוי התכונות עם wrapper מבוסס הבטחה בנוסף ל-methods הקודמות של navigator.webkitTemporaryStorage כדי לספק ממשק עקבי שמבוסס על:

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