# 逐出与尽力而为存储

> 浏览器可以不经询问删除哪些数据、Chrome 与 Firefox 的 LRU 顺序、Safari 的七天规则与主屏幕例外，以及存储桶为何整体清除。

在浏览器授予持久化之前，每个源的存储桶都是尽力而为（best-effort）模式。磁盘不足、浏览器总上限被触及，或者在 Safari 里用户连续七个浏览器使用天数没有与站点交互时，尽力而为的存储桶会被整体删除。删除的单位是存储桶，所以被逐出的源会同时失去缓存、数据库和 Service Worker 注册。

## 工作原理

Storage 标准定义了 `best-effort` 和 `persistent` 两种桶模式，并要求存储桶一旦被清除就必须整体清除（[Storage Standard](https://storage.spec.whatwg.org/)，whatwg.org）。触发条件和顺序由浏览器决定，各引擎在这里分道。

### Chromium 与 Firefox 的压力驱动逐出

Chromium 在总上限（磁盘总容量的 80%）被超过时开始逐出，Firefox 在磁盘被填满时开始。两者都按最近最少使用的顺序：最久未访问的尽力而为源先被清除，然后是下一个，直到用量回到上限以下（[Storage quotas and eviction criteria](https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria)，developer.mozilla.org）。因此一个源远低于自身配额时也可能被逐出：触发条件是所有源的合计用量，选择依据是访问时间而不是体积。持久化存储桶会被跳过。

### Safari 的七天规则

WebKit 有三个逐出触发条件：超过总配额（浏览器应用为磁盘的 80%，其他内嵌网页内容的应用为 20%）、系统存储压力，以及智能跟踪防护（ITP）（[Updates to Storage Policy](https://webkit.org/blog/14403/updates-to-storage-policy/)，webkit.org）。ITP 触发自 iOS 13.4 和 Safari 13.1 起生效：用户连续七个 Safari 使用天数没有与站点交互，该源由脚本写入的存储即被删除。计时按「使用了 Safari 的天数」而非日历天数，覆盖 IndexedDB、localStorage、sessionStorage、媒体密钥以及 Service Worker 注册和缓存（[Full Third-Party Cookie Blocking and More](https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/)，webkit.org）。源有打开的页面或存储桶处于持久化模式时不会被逐出。

添加到主屏幕的 Web App 运行在 Safari 之外，有自己独立的使用天数计数，打开已安装的应用就会重置计时；Apple 表示不预期此类应用的第一方数据被删除。

### 隐私浏览

隐私窗口是另一套生命周期。Chrome 的无痕模式把源配额降到约磁盘的 5%，并在最后一个无痕窗口关闭时丢弃数据（[Storage for the web](https://web.dev/articles/storage-for-the-web)，web.dev）；Firefox 和 Safari 同样在隐私会话结束时清除存储。这都不是 LRU 逐出，`persist()` 也不会改变它。

## 实测行为

逐出不会在页面里留下任何事件，只有间接信号，而每一条都不需要特殊工具就能验证。

- 逐出之后，`navigator.storage.estimate()` 报告该源的 `usage` 接近 0 而 `quota` 不变，因为配额是磁盘比例，只是桶空了。
- 存储桶被清除的源上，`caches.keys()` 解析为 `[]`，`indexedDB.databases()` 也解析为 `[]`（BCD `api.IDBFactory.databases`：Chrome 72、Firefox 126、Safari 14）。Service Worker 可以据此发现自己在 `install` 时写入的预缓存已经不在，并在下一次 `activate` 时重建。
- Safari 的七天删除同时移除 Service Worker 注册，所以超期后的访问从 `navigator.serviceWorker.controller === null` 开始，`install` 事件重新执行。

下面的检查在页面加载时运行，借 `localStorage` 里的一个标记区分「被逐出」和「首次访问」：Safari 会在同一次清理中连它一起清掉，而 Chromium 和 Firefox 逐出受配额管理的存储桶时不会动它。

```js
async function detectEviction() {
  if (!('caches' in self) || !('storage' in navigator)) {
    return 'unknown'; // 这里没有受配额管理的存储：无从检测
  }
  const names = await caches.keys();
  const seenBefore = localStorage.getItem('shell-installed') === '1';
  if (names.length === 0 && seenBefore) {
    return 'evicted'; // 存储桶在压力下被清除；重建预缓存
  }
  localStorage.setItem('shell-installed', '1');
  return names.length === 0 ? 'first-visit' : 'intact';
}
```

得到 `'evicted'` 就该重新执行预缓存，并在应用已经赢得资格时调用 `navigator.storage.persist()`，让下一次清理跳过这个源。

:::observed
Chrome DevTools 的 Application > Storage 面板无需填满磁盘即可复现压力场景：勾选 **Simulate custom storage quota**，输入低于当前用量的数值，下一次写入即以 `QuotaExceededError` 拒绝（[What's New in DevTools (Chrome 88)](https://developer.chrome.com/blog/new-in-devtools-88)，developer.chrome.com）。该覆盖只压低本源的配额，不会触发对其他源的 LRU 逐出，所以它测的是写入失败路径，不是删除路径。
:::

## 另请参阅

- [Storage Standard: buckets and eviction](https://storage.spec.whatwg.org/)（whatwg.org）
- [Updates to Storage Policy](https://webkit.org/blog/14403/updates-to-storage-policy/)（webkit.org）
- [Full Third-Party Cookie Blocking and More](https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/)（webkit.org）
- [StorageManager.persist() 与 persisted()](/zh/reference/storage/persistence/)
- [StorageManager.estimate()](/zh/reference/storage/quota-estimate/)
- [CacheStorage 与 caches 全局对象](/zh/reference/storage/cache-storage/)