Web Storage

在浏览器中存储数据有许多不同的选项。哪个选项最符合您的需求?

在路上时互联网连接会变得不稳定或无法上网,这是离线支持和可靠的性能成为渐进式 Web 应用中的常用功能的原因。即使在完美的无线环境中,明智地使用缓存和其他存储技术也可显著改善用户体验。您可以通过多种方式缓存静态应用资源(HTML、JavaScript、CSS、图片等)和数据(用户数据、新闻报道等)。但哪种解决方案是最佳方案?您可以存储多少数据?如何防止它被逐出?

我应该使用哪种设备?

以下是针对存储资源的一般建议:

所有现代浏览器都支持 IndexedDB、OPFS 和 Cache Storage API。它们是异步的,不会阻塞主线程(但 OPFS 还有一种同步变体,仅适用于 Web 工作器)。您可以从 window 对象、Web 工作器和 Service Worker 访问它们,因此您可以在代码中的任何位置使用它们。

其他存储机制是怎样的?

浏览器中还有其他几种存储机制,但它们的用途有限,并且可能会导致严重的性能问题。

SessionStorage 因标签页而异,并且其作用域仅限于标签页的生命周期。它对于存储少量会话特定信息(例如 IndexedDB 键)可能很有用。应谨慎使用它,因为它是同步的,并且会阻塞主线程。其大小限制为约 5MB,并且只能包含字符串。由于它是标签页特有的,因此无法通过 Web Worker 或 Service Worker 访问。

应避免使用 LocalStorage,因为它是同步的,并且会阻塞主线程。其大小上限约为 5MB,且只能包含字符串。无法从 Web 工作器或 Service Worker 访问 LocalStorage。

Cookie 有其用途,但不应用于存储。Cookie 会随每个 HTTP 请求一起发送,因此存储少量数据以外的任何内容都会显著增加每个网络请求的大小。它们是同步的,并且无法从 Web Worker 进行访问。与 LocalStorage 和 SessionStorage 一样,Cookie 仅限于字符串。

File System Access API 旨在让用户能够读取和修改其本地文件系统中的文件。用户必须授予权限,网页才能读取或写入任何本地文件,并且除非文件句柄缓存在 IndexedDB 中,否则权限不会跨会话保留。File System Access API 最适合编辑器等用例,在这些用例中,您需要打开文件、修改文件,然后可能需要将对文件所做的更改保存回来。

File System API 和 FileWriter API 提供了用于向沙盒化文件系统读取和写入文件的方法。虽然它是异步的,但不建议这样做,因为它仅适用于基于 Chromium 的浏览器

我可以存储多少数据?

总之,很多,至少几百兆字节,并且可能有数百 GB 甚至更多。各个浏览器实现情况会有所不同,但可用存储空间的大小通常取决于设备上的可用存储空间量。

  • Chrome 允许浏览器最多使用 80% 的总磁盘空间。一个源最多可以使用总磁盘空间的 60%。您可以使用 StorageManager API 来确定可用的配额上限。其他基于 Chromium 的浏览器可能有所不同。
    • 在无痕模式下,Chrome 会将来源可使用的存储空间量减少到总磁盘空间的约 5%。
    • 如果用户在 Chrome 中启用了“关闭所有窗口时清除 Cookie 和网站数据”,则存储空间配额会大幅减少到大约 300MB。
  • Firefox 允许浏览器最多使用 50% 的可用磁盘空间。一个 eTLD+1 组(例如,example.comwww.example.comfoo.bar.example.com最多可使用 2GB。您可以使用 StorageManager API 来确定仍有多少可用空间。
  • Safari(桌面版和移动版)似乎允许大约 1GB 空间。达到上限后,Safari 会提示用户,并以 200MB 为增量增加上限。我找不到任何关于此问题的官方文档。
    • 如果将 PWA 添加到移动版 Safari 的主屏幕,系统会创建一个新的存储容器,并且 PWA 和移动版 Safari 之间不会共享任何内容。安装的 PWA 达到配额后,似乎无法再申请额外的存储空间。

过去,如果网站存储的数据量超过特定阈值,浏览器会提示用户授予使用更多数据的权限。例如,如果来源使用了超过 50MB 的空间,浏览器会提示用户允许其存储最多 100MB 的数据,然后以 50MB 为增量再次询问。

目前,大多数新型浏览器都不会向用户发出提示,并且会允许网站使用其分配的配额。Safari 似乎属于例外情况,当超出存储配额时,它会发出提示,请求增加分配的配额。如果来源尝试使用的配额超出其分配的配额,则进一步尝试写入数据将会失败。

如何查看可用存储空间?

许多浏览器中,您可以使用 StorageManager API 来确定来源可用的存储空间量以及其使用的存储空间量。它可报告 IndexedDB 和 Cache API 使用的字节总数,并可用于计算大致的可用剩余存储空间。

if (navigator.storage && navigator.storage.estimate) {
  const quota = await navigator.storage.estimate();
  // quota.usage -> Number of bytes used.
  // quota.quota -> Maximum number of bytes available.
  const percentageUsed = (quota.usage / quota.quota) * 100;
  console.log(`You've used ${percentageUsed}% of the available storage.`);
  const remaining = quota.quota - quota.usage;
  console.log(`You can write up to ${remaining} more bytes.`);
}

您必须捕获配额超限错误(见下文)。在某些情况下,可用配额可能会超过实际的可用存储空间量。

检查

在开发过程中,您可以使用浏览器的开发者工具检查不同的存储类型,并清除所有存储的数据。

Chrome 88 中新增了一项功能,可让您在“存储”窗格中替换网站的存储配额。借助此功能,您可以模拟不同的设备,并在磁盘可用性较低的情况下测试应用的行为。依次前往应用存储空间,选中模拟自定义存储空间配额复选框,然后输入任何有效数字以模拟存储空间配额。

在编写本指南的过程中,我编写了一个简单的工具,尝试快速使用尽可能多的存储空间。这是一种快速尝试不同存储机制的方法,可让您了解在用尽配额后会发生什么情况。

如何处理超出配额的问题?

超出配额后该怎么办?最重要的是,您应始终捕获和处理写入错误,无论是 QuotaExceededError 还是其他错误。然后,根据您的应用设计,决定如何处理它。例如,删除很长时间未访问的内容、根据大小移除数据,或提供一种方式供用户选择要删除的内容。

如果您超出了可用配额,IndexedDB 和 Cache API 都会抛出名为 QuotaExceededErrorDOMError

IndexedDB

如果来源超出了配额,则尝试写入 IndexedDB 将会失败。系统会调用事务的 onabort() 处理脚本,并传递事件。该事件将在 error 属性中包含 DOMException。检查错误 name 将返回 QuotaExceededError

const transaction = idb.transaction(['entries'], 'readwrite');
transaction.onabort = function(event) {
  const error = event.target.error; // DOMException
  if (error.name == 'QuotaExceededError') {
    // Fallback code goes here
  }
};

Cache API

如果来源超出了配额,则尝试写入 Cache API 将被拒绝并返回 QuotaExceededError DOMException

try {
  const cache = await caches.open('my-cache');
  await cache.add(new Request('/sample1.jpg'));
} catch (err) {
  if (error.name === 'QuotaExceededError') {
    // Fallback code goes here
  }
}

逐出是如何运作的?

网站存储分为两个存储分区:“尽最大努力”和“永久性”。“尽力而为”是指浏览器可以在不干扰用户的情况下清除存储空间,但对于长期或关键数据而言持久性较差。持久化存储在存储容量低时不会自动清除。用户需要手动清除此存储(通过浏览器设置)。

默认情况下,网站的数据(包括 IndexedDB、Cache API 等)属于“尽力而为”类别,这意味着,除非网站请求了永久性存储空间,否则浏览器可能会自行驱逐网站数据(例如,当设备存储空间不足时)。

尽力而为的驱逐政策如下:

  • 基于 Chromium 的浏览器会在浏览器的存储空间用尽后开始逐出数据,即先从最近最少使用的来源中清除所有网站数据,然后在下一个位置清除,直到浏览器不再超出限制。
  • 当可用磁盘空间用尽时,Firefox 会开始逐出数据,首先清除最久未使用的来源中的所有网站数据,然后依次清除其他来源中的数据,直到浏览器不再超出限制。
  • Safari 之前不会驱逐数据,但最近对所有可写入存储空间实施了新的 7 天上限(见下文)。

从 iOS 和 iPadOS 13.4 以及 macOS 上的 Safari 13.1 开始,所有脚本可写存储空间(包括 IndexedDB、服务工作线程注册和 Cache API)的存储期限上限均为 7 天。这意味着,如果用户在使用 Safari 七天后未与网站互动,Safari 将从缓存中移除所有内容。此逐出政策不适用于已添加到主屏幕的已安装 PWA。如需了解完整详情,请参阅 WebKit 博客上的全面屏蔽第三方 Cookie 等

存储桶

Storage Buckets API 的核心理念是授予网站创建多个存储桶的权限,浏览器可以选择独立于其他存储桶来删除每个存储桶。这样,开发者就可以指定驱逐优先级,以确保不会删除最有价值的数据。

额外知识点:为何对 IndexedDB 使用封装容器

IndexedDB 是一种低级 API,需要在使用前进行大量设置,这对于存储复杂性较低的数据来说尤其麻烦。与大多数基于 promise 的现代 API 不同,它基于事件。IndexedDB 的 Promise 封装容器(例如 idb)会隐藏一些强大的功能,但更重要的是,会隐藏 IndexedDB 库自带的一些复杂机制(例如事务处理、架构版本)。

奖励:SQLite Wasm

在 Web SQL 被弃用并从 Chrome 中移除后,Google 与广受欢迎的 SQLite 数据库的维护人员合作,推出了基于 SQLite 的 Web SQL 替代方案。如需详细了解如何使用它,请参阅在由源私有文件系统支持的浏览器中使用 SQLite Wasm

总结

存储空间有限且不断提示用户存储更多数据的时代已经一去不返。网站可以有效存储运行所需的所有资源和数据。您可以使用 StorageManager API 确定您有多少可用存储空间,以及您已使用的存储空间。借助永久性存储空间,除非用户将其移除,否则您可以保护其免遭驱逐。

其他资源

谢谢

特别感谢 Jarryd Goodman、Phil Walton、Eiji Kitamura、Daniel Murphy、Darwin Huang、Josh Bell、Marijn Kruisselbrink 和 Victor Costan 对本指南的审核。感谢 Eiji Kitamura、Addy Osmani 和 Marc Cohen 撰写了本文所依据的原始文章。Eiji 编写了一个名为 Browser Storage Abuser 的实用工具,该工具有助于验证当前行为。这样,您就可以尽可能存储更多数据,并查看浏览器的存储限制。感谢 François Beaufort 深入了解 Safari,确定其存储限制,并感谢 Thomas Steiner 在 2024 年添加了有关源私有文件系统、存储分区、SQLite Wasm 的信息以及整体内容更新。

主打图片由 Unsplash 上的 Guillaume Bolduc 提供。