Storage Quota API 让 PWA 在填满空间之前检查配额
猜测存储限制行不通。以下是 PWA 在填满空间前先询问的做法。
为什么存储配额对 PWA 很重要
浏览器对网络应用实施存储限制,以防止滥用并保护用户设备。当你填满配额后,浏览器可能开始驱逐你的数据——可能在对用户最糟糕的时刻。
Storage Quota API(navigator.storage)为 PWA 提供了一种方式来:
- 写入大文件或缓存前检查可用空间
- 请求持久化存储以降低驱逐风险
- 监控使用情况以保持安全限制内
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 文档大致总结驱逐规则为:
- 驱逐发生在设备存储严重不足时
- 媒体内容(视频、音频)更可能被驱逐
- 驱逐对 origin 是整体的——对一个 given origin 或全删或全不删
- 持久化有影响:持久化的 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 写入的数据位于浏览器配额生态系统之外(用户授权的文件存储在盘上),不影响 quota 或 usage:
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();
}
}下一步
- 识别应用中的高价值用户数据并请求持久化
- 添加
estimate()调用于大型 IndexedDB 写入及缓存操作前 - 实现缓存清理策略(LRU、基于年龄)
- 通过填满存储测试驱逐并观察什么先被移除
- 记录存储预算并在生产中监控使用量
存储不是无限的。在填满之前听浏览器告诉你的信息。