OpenPWAStore
返回 News
Guide · May 20, 2026

Storage Quota API 让 PWA 在填满空间之前检查配额

猜测存储限制行不通。以下是 PWA 在填满空间前先询问的做法。

OpenPWA Editorial3 min read
Storage Quota API 让 PWA 在填满空间之前检查配额 cover

为什么存储配额对 PWA 很重要

浏览器对网络应用实施存储限制,以防止滥用并保护用户设备。当你填满配额后,浏览器可能开始驱逐你的数据——可能在对用户最糟糕的时刻。

Storage Quota API(navigator.storage)为 PWA 提供了一种方式来:

  1. 写入大文件或缓存前检查可用空间
  2. 请求持久化存储以降低驱逐风险
  3. 监控使用情况以保持安全限制内

MDN 的 Navigator 文档说明,navigator.storage 返回用于管理持久化权限和估计可用存储的 StorageManager 单例。

Storage Quota API 如何工作

核心方法

const storageManager = navigator.storage;

// 估计使用量与配额
const { usage, quota } = await storageManager.estimate();

console.log(`已使用: ${usage} 字节 (${(usage / 1024 / 1024).toFixed(2)} MB)`);
console.log(`可用: ${quota} 字节 (${(quota / 1024 / 1024).toFixed(2)} MB)`);
console.log(`剩余: ${quota - usage} 字节`);

返回数据

  • usage:你的 origin 大致已占用字节(包括 IndexedDB、Cache API 等)
  • quota:你的 origin 大致可用总字节

请求持久化存储

async function requestPersistentStorage() {
  if (navigator.storage && navigator.storage.persist) {
    const persisted = await navigator.storage.persist();
    console.log(`存储已持久化: ${persisted}`);
    return persisted;
  }
  return false;
}

注意:在许多浏览器中(尤其是桌面版 Chrome),调用 persist() 会显示用户权限提示。用户可以拒绝。

检查存储是否已持久化

const isPersisted = await navigator.storage.persisted();
console.log(`存储是否持久化? ${isPersisted}`);

何时使用 Storage Quota API

缓存大资源之前

async function cacheLargeVideo(url) {
  const videoSizeMB = 85; // 估计值
  const { usage, quota } = await navigator.storage.estimate();
  const availableMB = (quota - usage) / 1024 / 1024;

  if (availableMB < videoSizeMB + 10) { // 10 MB 缓冲区
    console.warn('离线视频存储空间不足');
    // 通知用户并提供替代方案
    showStorageWarning();
    return false;
  }

  const cache = await caches.open('video-cache');
  const response = await fetch(url);
  if (response.ok) {
    await cache.put(url, response);
    return true;
  }
  return false;
}

保存用户生成内容之前

async function saveUserPhoto(file) {
  const { usage, quota } = await navigator.storage.estimate();
  const fileSize = file.size;

  if (quota - usage < fileSize * 1.5) { // 1.5 倍缓冲区
    alert('保存照片的存储空间不足。尝试释放空间或更小的图片。');
    return false;
  }

  // 保存到 IndexedDB……
}

应用加载时(监控)

async function checkStorageOnLoad() {
  const { usage, quota } = await navigator.storage.estimate();
  const percentUsed = (usage / quota) * 100;

  if (percentUsed > 80) {
    showStorageWarning(`已使用可用存储的 ${percentUsed.toFixed(0)}%。清理以避免数据丢失。`);
  }

  return percentUsed;
}

存储驱逐规则

浏览器在存储压力高时优先驱逐较小、轻量的 origin:

Chromium 型浏览器(Chrome 等)

MDN 与 Chrome 文档大致总结驱逐规则为:

  1. 驱逐发生在设备存储严重不足
  2. 媒体内容(视频、音频)更可能被驱逐
  3. 驱逐对 origin 是整体的——对一个 given origin 或全删或全不删
  4. 持久化有影响:持久化的 origin 更少被驱逐

Safari

Safari 的政策更激进:

  • iOS Safari 可在大约 7 天后从不常使用的应用中清理数据
  • 桌面 Safari 对每个 origin 执行更严格的限制
  • 持久化请求不太显眼/隐藏

Firefox

Firefox 优先级基于:

  • 使用量
  • 访问频率
  • 权限授予(存储持久化)

持久化策略清单

何时请求持久化

| 场景 | 请求持久化? | 理由 | |------|-----------|------| | 拥有关键用户数据的离线优先 PWA | ✅ 强烈推荐 | 保护避免驱逐 | | 内容重应用(视频、杂志) | ✅ 是 | 驱逐可能性高 | | 简单书签应用 | ⚠️ 可选 | 风险更低 | | 多用户或共享设备 | ❌ 避免 | 用户可能不希望持久化存储 | | 公共亭 PWA | ❌ 不 | 不合适 |

用户同意最佳实践

async function requestPersistenceWithExplanation() {
  const persisted = await navigator.storage.persist();

  if (!persisted) {
    // 用户拒绝或浏览器未提示
    showEmail(
      `我们更倾向于持久化存储来保护你的数据,但我们也可以不用。你可以在网站设置中启用。`
    );
  } else {
    console.log('存储已持久化');
  }

  return persisted;
}

建议

  • 在请求持久化前解释为什么这很重要
  • 若被拒绝则提供替代
  • 不要在同一会话中反复提示同一用户

IndexedDB 与存储配额集成

IndexedDB 使用估计

IndexedDB 事务可能很大。批量插入前先估计:

async function bulkInsertRecords(records, dbName, storeName) {
  const { usage, quota } = await navigator.storage.estimate();
  const estimatedSize = records.length * averageRecordSize;

  if (quota - usage < estimatedSize * 1.2) {
    alert('记录太多。尝试分批上传或删除旧数据。');
    return;
  }

  const db = await openDB(dbName);
  const tx = db.transaction(storeName, 'readwrite');
  const store = tx.objectStore(storeName);

  for (const record of records) {
    await store.add(record);
  }
}

IndexedDB + Cache API 共存

IndexedDB 与 Cache API 共享同一存储配额。你的 usage 包含两者:

const { usage } = await navigator.storage.estimate();

// 大致分解(你需要应用内跟踪):
const indexedDBUsage = await trackIndexedDBUsage();
const cacheAPIUsage = usage - indexedDBUsage;

console.log(`IndexedDB: ${indexedDBUsage} 字节`);
console.log(`Cache API: ${cacheAPIUsage} 字节`);

PWA 特定考虑因素

离线缓存 + 用户数据

对于离线优先 PWA,Cache API 中的应用壳与 IndexedDB 中的用户数据竞争空间:

| 优先级 | 存储类型 | 原因 | |--------|---------|------| | 1 | 用户生成内容(IndexedDB) | 对用户价值最高,较难以重建 | | 2 | 关键资源(HTML、CSS、JS 在 Cache API) | 应用稳定性 | | 3 | 不错资源(图片、视频) | 较低驱逐优先级 |

提示:定期清理 Cache API 中的停滞资源,为 IndexedDB 释放空间。

File System Access + 存储配额

File System Access API 写入的数据位于浏览器配额生态系统之外(用户授权的文件存储在盘上),不影响 quotausage

const fileHandle = await window.showSaveFilePicker();
// 向此文件写入不计入 navigator.storage

Origin Private File System (OPFS) 的使用量会计入:

const root = await navigator.storage.getDirectory();
// 这里的写入**计入**配额

模式化做法

模式 1:存储感知缓存

async function smartCache(urls) {
  const { usage, quota } = await navigator.storage.estimate();
  const budgetMB = 100; // 应用预算(策略)
  const usedMB = usage / 1024 / 1024;

  if (usedMB > budgetMB) {
    console.log('超预算。LRU 清理最旧的缓存……');
    await lruEvictCaches(-10); // 释放 10 MB
  }

  const cache = await caches.open('dynamic-cache');
  cache.addAll(urls);
}

模式 2:渐进式存储监控

// 每 5 分钟监控一次
setInterval(async () => {
  const { usage, quota } = await navigator.storage.estimate();
  const percentUsed = (usage / quota) * 100;

  if (percentUsed > 90) {
    warnUser(`存储严重填满:已用 ${percentUsed.toFixed(0)}%。请删除旧数据。`);
  }
}, 5 * 60 * 1000);

模式 3:应用更新前存储检查

async function beforeAppUpdate(newAssets) {
  const { usage, quota } = await navigator.storage.estimate();
  const requiredSpace = newAssets.reduce((sum, asset) => sum + asset.size, 0);

  if (quota - usage < requiredSpace * 1.3) {
    alert('更新应用的存储空间不足。先清理一些离线内容。');
    return false;
  }

  return true;
}

验证清单

生产前:

  • [ ] 大写入前调用 navigator.storage.estimate()
  • [ ] 关键应用请求 navigator.storage.persist()
  • [ ] 持久化提示前用户看到清晰解释
  • [ ] 缓存清理策略已定义(LRU、基于年龄)
  • [ ] 批量插入前估计 IndexedDB 使用
  • [ ] 超预算场景显示友好警告
  • [ ] 易驱逐内容已标记(例如“下载内容”)

浏览器兼容性

| Browser | estimate() | persist() | persisted() | |---------|--------------|-------------|---------------| | Chrome | ✅ | ✅(显示提示) | ✅ | | Edge | ✅ | ✅ | ✅ | | Firefox | ✅ | ✅ | ✅ | | Safari | ⚠️ 有限 | ❌ | ❌ |

注意:Safari 不支持 persist()persisted()。存储限制执行更激进,且无用户授予。

调试存储

DevTools Application 标签

  • Storage:查看 IndexedDB、Cache API、本地存储配额
  • Storage Buckets:(高级)细粒度配额管理
  • Clear Storage:用于测试驱逐与重新缓存

CLI/Console 测试

// 快速估计
navigator.storage.estimate().then(console.log);

// 检查持久化
navigator.storage.persisted().then(console.log);

// 测试权限
navigator.storage.persist().then(console.log);

常见陷阱

1. 假设无限存储

现代浏览器执行per-origin的有限且易驱逐配额。

2. 大写入前未检查

写入 50 MB 数据而不检查存储可能操作中途失败。

3. 忽视 usage 中的 Cache API

你的 usage 包含所有 Cache API 缓存——它们不是免费的。

4. 忘记 Safari 差异

Safari 缺少持久化 API,且可能更激进地驱逐。

5. 写入时静默失败

配额超限时要始终处理 IndexedDB/Cache API 的拒绝:

try {
  await db.add('myStore', hugeData);
} catch (err) {
  if (err.name === 'QuotaExceededError') {
    showQuotaExceededMessage();
  }
}

下一步

  1. 识别应用中的高价值用户数据并请求持久化
  2. 添加 estimate() 调用于大型 IndexedDB 写入及缓存操作前
  3. 实现缓存清理策略(LRU、基于年龄)
  4. 通过填满存储测试驱逐并观察什么先被移除
  5. 记录存储预算并在生产中监控使用量

存储不是无限的。在填满之前听浏览器告诉你的信息。