浏览器的数据存储方法比较

/ 默认分类 / 没有评论 / 213浏览

现代浏览器中可用的存储 API

首先,让我们简要概述一下不同的 API、它们的目的用例和历史:

什么是 Cookies

Cookies 首次由netscape在 1994 年引入。Cookies 存储一些键值数据的小片段,主要用于会话管理、个性化跟踪。Cookies 可以设置多个安全选项,如生存时间或域名属性,以便在多个子域之间共享 Cookies。

Cookies 的值不仅存储在客户端,还与每个 HTTP 请求一起发送到服务器。这意味着我们无法在 Cookie 中存储大量数据,但与其他方法相比,Cookie 的访问性能仍然很有趣。特别是由于 Cookie 是网络的一个重要基本功能,已经进行了许多性能优化,甚至在这些日子里,像 chromium 的共享内存版本或异步的CookieStore API这样的进步仍在继续。

什么是 LocalStorage

localStorage API 首次于 2009 年作为[WebStorage 规范的一部分被提出。LocalStorage 提供了一个简单的 API,用于在网页浏览器内部存储键值对。它具有setItemgetItemremoveItemclear等方法,这些都是从键值存储中所需的所有功能。LocalStorage 仅适用于存储需要跨会话持久的小量数据,并且它受到5MB 存储容量的限制。存储复杂数据只能通过将其转换为字符串来实现,例如使用JSON.stringify()。该 API 不是异步的,这意味着在进行操作时将完全阻塞您的 JavaScript 进程。因此,在它上面运行重操作可能会阻止 UI 渲染。

也存在 SessionStorage API。主要区别在于 localStorage 数据会无限期持续,直到明确清除,而 sessionStorage 数据在浏览器标签页或窗口关闭时会被清除。

什么是 IndexedDB

IndexedDB 首次作为“索引数据库 API”于 2015 年推出。

IndexedDB是一个用于存储大量结构化 JSON 数据的低级 API。虽然该 API 使用起来有些困难,但 IndexedDB 可以利用索引和异步操作。它不支持复杂查询,并且只允许遍历索引,这使得它更像是一个其他库的基础层,而不是一个完整的数据库。

2018 年,引入了 IndexedDB 版本 2.0 。这增加了一些主要改进。最显著的是getAll()方法,在获取大量 JSON 文档时显著提高了性能。

IndexedDB 版本 3.0正在开发中,其中包含许多改进。最重要的是增加了基于Promise的调用,这使得现代 JS 特性如async/await更加有用。

什么是 OPFS

原始私有文件系统》(OPFS)是一个相对较新的API,允许 Web 应用程序直接在浏览器中存储大文件。它旨在为想要在模拟文件系统中写入和读取二进制数据的数据密集型应用程序设计。

OPFS 可以使用两种模式:

因为只有二进制数据可以被处理,OPFS 被设计成库开发者的基础文件系统。当你构建一个“正常”的应用程序时,你不太可能直接在你的代码中使用 OPFS,因为它太复杂了。这仅适用于存储像图像这样的普通文件,而不是高效地存储和查询 JSON 数据。我为 RxDB 构建了一个基于 OPFS 的存储,并进行了适当的索引和查询,这花了我几个月的时间。

什么是 WASM SQLite

info_files_icons_sqlite.svg WebAssembly(Wasm)是一种允许在网络上执行高性能代码的二进制格式。Wasm 于 2017 年被添加到主流浏览器中,这为在浏览器内部运行的内容开辟了广泛的机会。您可以将本地库编译成 WebAssembly,只需进行少量调整即可在客户端运行。WASM 代码可以发送到浏览器应用程序,通常比 JavaScript 运行得快得多,但仍然比本地代码慢约10%

许多人开始将编译后的 SQLite 用作浏览器内的数据库,这就是为什么将这种设置与原生 API 进行比较也很有意义。

SQLite 编译的字节码大小约为 938.9 KB,必须在第一次页面加载时由用户下载并解析。WASM 不能直接访问浏览器中的任何持久存储 API。相反,它需要数据从 WASM 流向主线程,然后才能放入浏览器 API 之一。这是通过所谓的虚拟文件系统(VFS)适配器来完成的,它处理从 SQLite 到其他任何数据访问。

什么是 WebSQL

WebSQL 曾是一个在 2009 年引入的 Web API,允许浏览器使用基于 SQLite 的 SQL 数据库进行客户端存储。该想法是为开发者提供一种在客户端使用 SQL 存储和查询数据的方法,类似于服务器端数据库。由于多个良好原因,WebSQL 在近年已被从浏览器中移除

因此,在以下内容中,我们甚至会忽略 WebSQL,即使通过设置特定的浏览器标志或使用旧版本的 Chromium 来运行测试也是可能的。

功能比较

现在您已经了解了 API 的基本概念,让我们比较一些对使用 RxDB 和基于浏览器的存储的人来说非常重要的特定功能。

存储复杂的 JSON 文档

当你在一个 Web 应用程序中存储数据时,通常你想要存储复杂的 JSON 文档,而不仅仅是存储在服务器端数据库中的“正常”值,如整数和字符串。

每个其他 API 只能存储字符串或二进制数据。当然,您可以使用JSON.stringify()将任何 JSON 对象转换为字符串,但如果没有在 API 中支持 JSON,则在运行查询时会使事情变得复杂。多次运行JSON.stringify()可能会引起性能问题。

多标签支持

构建 Web 应用与Electron或React-Native相比的一个显著区别是,用户将同时在多个浏览器标签中打开和关闭应用。因此,您不仅有一个 JavaScript 进程在运行,而且可能有多个进程存在,并且可能需要在它们之间共享状态变化,以避免向用户显示过时数据。

如果你的用户在浏览你的网站时,肌肉记忆让左手放在了键上,那么你肯定做错了什么!

并非所有存储 API 都支持在标签页之间自动共享写事件的方式。

只有 localStorage 有通过 API 本身以自动在标签页之间共享写事件的方式,该事件可用于观察变化。

// localStorage can observe changes with the storage event.
// This feature is missing in IndexedDB and others
addEventListener("storage", (event) => {});

存在一个用于 Chrome 的实验性 IndexedDB 观察者 API,但该提案仓库已被存档。

为了解决这个问题,有两种解决方案:

索引支持

数据库与在普通文件中存储数据之间的主要区别在于,数据库以允许在索引上运行操作以简化快速查询的格式写入数据。在我们的技术列表中,只有 IndexedDBWASM SQLite 支持开箱即用的索引功能。从理论上讲,您可以在任何存储上构建索引,如 localstorage 或 OPFS,但您可能不想自己这样做。

在 IndexedDB 中,例如,我们可以通过给定的索引范围获取大量文档:

// find all products with a price between 10 and 50
const keyRange = IDBKeyRange.bound(10, 50);
const transaction = db.transaction('products', 'readonly');
const objectStore = transaction.objectStore('products');
const index = objectStore.index('priceIndex');
const request = index.getAll(keyRange);
const result = await new Promise((res, rej) => {
  request.onsuccess = (event) => res(event.target.result);
  request.onerror = (event) => rej(event);
});

请注意,IndexedDB 有一个限制,即布尔值没有索引。您只能索引字符串和数字。为了解决这个问题,您需要在存储数据时将布尔值转换为数字,并在读取时反向转换。

WebWorker 支持

在运行大量数据处理操作时,您可能希望将处理过程从 JavaScript 主线程移开。这确保了我们的应用始终保持响应和快速,同时处理可以在后台并行运行。在浏览器中,您可以使用WebWorkerSharedWorkerServiceWorker API 来完成此操作。在 RxDB 中,您可以使用WebWorker或SharedWorker插件将您的存储移动到工作线程内部。

该用例最常用的 API 是通过创建一个WebWorker并在第二个 JavaScript 进程中完成大部分工作。这个工作进程是从一个单独的 JavaScript 文件(或 base64 字符串)中生成的,并通过使用postMessage()发送数据与主线程进行通信。

很不幸,由于设计和安全限制,LocalStorageCookies 以及 不能在 WebWorker 或 SharedWorker 中使用。WebWorkers 在与主浏览器线程分离的独立全局上下文中运行,因此无法执行可能影响主线程的操作。它们无法直接访问某些 Web API,如 DOM、localStorage 或 cookies。

所有其他内容都可以在 WebWorker 内部使用。带有createSyncAccessHandle方法的 OPFS 快速版本可以仅在 WebWorker 中使用不能在主线程上使用。这是因为返回的AccessHandle的所有操作都是非异步的,因此会阻塞 JavaScript 进程,所以你不想在主线程上执行并阻塞一切。

存储大小限制

性能比较

现在我们已经审查了每种存储方法的特性,让我们深入了解性能比较,重点关注初始化时间、读写延迟和批量操作。

请注意,我们只运行简单的测试,并且对于您在应用程序中的特定用例,结果可能会有所不同。此外,我们只在谷歌 Chrome(版本 128.0.6613.137)中比较性能。Firefox 和 Safari 有类似但并不完全相同的性能模式。您可以从这个GitHub 仓库自行在您的机器上运行测试。对于所有测试,我们将网络速度限制为平均德国互联网速度。(下载:135,900 kbit/s,上传:28,400 kbit/s,延迟:125ms)。此外,所有测试都存储一个“平均”的 JSON 对象,根据存储可能需要将其转换为字符串。我们还只测试通过 id 存储文档的性能,因为某些技术(cookies、OPFS 和 localStorage)不支持索引范围操作,所以比较这些技术的性能没有意义。

初始化时间

在您存储任何数据之前,许多 API 需要设置过程,例如创建数据库、启动 WebAssembly 进程或下载额外内容。为了确保您的应用启动速度快,初始化时间很重要。

localStorage 和 Cookies 的 API 无需设置过程,可直接使用。IndexedDB 需要打开一个数据库及其内部的存储。WASM SQLite 需要下载 WASM 文件并处理它。OPFS 需要下载并启动一个工作文件,初始化虚拟文件系统目录。

这里是从第一次能够存储数据所需时间的时间测量结果:

技术时间(毫秒)
IndexedDB46
OPFS Main Thread23
OPFS WebWorker26.8
WASM SQLite (memory)504
WASM SQLite (IndexedDB)535

这里我们可以注意到一些事情:

小写延迟

接下来,让我们测试小写操作的延迟。当你进行许多相互独立的小数据更改时,这很重要。比如当你从 WebSocket 流数据或持久化伪随机发生的事件,如鼠标移动时。

技术时间(毫秒)
Cookies0.058
LocalStorage0.017
IndexedDB0.17
OPFS Main Thread1.46
OPFS WebWorker1.54
WASM SQLite (memory)0.17
WASM SQLite (IndexedDB)3.17

这里我们可以注意到一些事情:

OPFS 操作将 JSON 数据写入一个文档大约需要 1.5 毫秒。我们可以看到,首先将数据发送到 webworker 会稍微慢一些,这源于在双方序列化和反序列化数据时的开销。如果我们不是为每个文档创建一个 OPFS 文件,而是将所有内容追加到单个文件中,性能模式将发生显著变化。然后,从createSyncAccessHandle()创建的更快文件句柄每次写入只需大约 1 毫秒。但这需要以某种方式记住每个文档存储的位置。因此,在我们的测试中,我们将继续使用每个文档一个文件的方式。

小读操作延迟

现在我们已经存储了一些文档,让我们通过它们的id来测量读取单个文档所需的时间。

技术时间(毫秒)
Cookies0.132
LocalStorage0.0052
IndexedDB0.1
OPFS Main Thread1.28
OPFS WebWorker1.41
WASM SQLite (memory)0.45
WASM SQLite (IndexedDB)2.93

这里我们可以注意到一些事情:

大量写入

下一步,让我们一次性处理 200 份文档的大批量操作。

技术时间(毫秒)
Cookies20.6
LocalStorage5.79
IndexedDB13.41
OPFS Main Thread280
OPFS WebWorker104
WASM SQLite (memory)19.1
WASM SQLite (IndexedDB)37.12

这里我们可以注意到一些事情:

大量读取

现在让我们一次性读取 100 份文档。

技术时间(毫秒)
Cookies6.34
LocalStorage0.39
IndexedDB4.99
OPFS Main Thread54.79
OPFS WebWorker25.61
WASM SQLite (memory)3.59
WASM SQLite (IndexedDB)5.84 (35ms without cache)

这里我们可以注意到一些事情:

性能结论