浏览器的数据六种存储方法比较 :LocalStorage vs. IndexedDB vs. Cookies vs. OPFS vs. WASM-SQLite

在构建该 Web 应用程序,并且希望将数据存储在用户浏览器中。也许您只需要存储一些小标志,或者甚至需要一个成熟的数据库。

我们构建的 Web 应用程序类型发生了显着变化。在网络发展的早期,我们提供静态 html 文件。然后我们提供动态渲染的 html,后来我们构建在客户端上运行大部分逻辑的单页应用程序 。在未来几年中,您可能希望构建所谓的本地优先应用程序,仅在客户端上处理大而复杂的数据操作,甚至可以在离线状态下工作,这使您有机会构建零延迟的用户交互。

在网络的早期, cookie是存储小型键值分配的唯一选择。但是 JavaScript 和浏览器已经发生了显着的发展,并且添加了更好的存储 API,这为更大、更复杂的数据操作铺平了道路。

在本文中,我们将深入探讨可用于在浏览器中存储和查询数据的各种技术。我们将探索传统方法,如CookieslocalStorageWebSQLIndexedDB 和较新的解决方案,如通过 WebAssembly 的OPFSSQLite 。我们比较功能和限制,并通过性能测试来揭示使用各种方法在 Web 应用程序中写入和读取数据的速度。

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

首先,让我们简要概述不同的API,它们的意图用例和历史:

什么是Cookies

Cookies是netscape在1994年首次引入的。Cookie存储主要用于会话管理、个性化和跟踪的小块键值数据。Cookie可以有几个安全设置,如生存时间或domain属性,以便在几个子域之间共享Cookie。

Cookie值不仅存储在客户端,而且还随每个http请求发送到服务器。这意味着我们不能在cookie中存储太多数据,但与其他方法相比,cookie访问性能仍然很有趣。特别是因为cookie是Web的重要基础功能,许多性能优化已经完成,即使在这些天仍然有进展,如chromium的共享内存版本控制或异步CookieStore API

什么是LocalStorage

localStorage API最初是在2009年作为WebStorage规范的一部分提出的。LocalStorage提供了一个简单的API来在Web浏览器中存储键值对。它有方法setItemgetItemremoveItemclear,这是你从键值存储中所需要的。LocalStorage仅适合存储需要跨会话持久化的少量数据,并且受到5 MB存储上限的限制。存储复杂数据只能通过将其转换为字符串来实现,例如使用JSON.stringify()。API不是异步的,这意味着如果完全阻止你的JavaScript进程,而做的事情。因此,在其上运行繁重的操作可能会阻止您的UI呈现。

还有SessionStorage API。关键的区别在于localStorage数据无限期地保持,直到显式清除,而sessionStorage数据在浏览器选项卡或窗口关闭时被清除。

什么是IndexedDB

IndexedDB于2015年首次作为"索引数据库API"引入。

IndexedDB是一个用于存储大量结构化JSON数据的低级API。虽然API有点难以使用,但IndexedDB可以利用索引和异步操作。它缺乏对复杂查询的支持,只允许覆盖索引,这使得它更像是其他库的基础层,而不是一个完全成熟的数据库。

2018年,IndexedDB 2.0版本发布。这增加了一些重大改进。最值得注意的是getAll()方法,它在获取大量JSON文档时显着提高了性能。

IndexedDB3.0版本正在运行中,其中包含许多改进。最重要的是,添加了基于Promise的调用,使现代JS功能(如async/await)更加有用。

什么是OPFS

Origin Private File System(OPFS)是一个相对较新的API,它允许Web应用程序直接在浏览器中存储大型文件。它是为数据密集型应用程序而设计的,这些应用程序希望在模拟文件系统中写入和读取二进制数据。

OPFS可以在两种模式下使用:

  • 或者在主线程上异步
  • 或者在WebWorker中使用createSyncAccessHandle()方法进行更快的异步访问。

由于只能处理二进制数据,因此OPFS被用作库开发人员的基本文件系统。在构建"普通"应用程序时,您不太可能直接在代码中使用OPFS,因为它太复杂了。这只对存储像图像这样的普通文件有意义,而不是有效地存储和查询JSON数据。我已经为RxDB构建了一个基于OPFS的存储,并使用了适当的索引和查询,这花了我几个月的时间。

什么是WASM SQLite

WebAssembly(Wasm)是一种二进制格式,允许在Web上执行高性能代码。Wasm在2017年被添加到主要浏览器中,这为在浏览器中运行什么打开了广泛的机会。您可以将原生库编译为WebAssembly,并只需进行一些调整即可在客户端上运行它们。WASM代码可以被发送到浏览器应用程序,并且通常比JavaScript运行得更快,但仍然比原生代码慢10%左右。

许多人开始使用编译的SQLite作为浏览器内部的数据库,这就是为什么将此设置与本机API进行比较是有意义的。

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

什么是WebSQL

WebSQL是2009年推出的一种Web API,允许浏览器使用SQL数据库进行客户端存储,基于SQLite。这个想法是给开发人员一种在客户端使用SQL存储和查询数据的方法,类似于服务器端数据库。WebSQL近年来已经从浏览器中删除,原因有很多:

  • WebSQL尚未标准化,并且基于SQLite源代码形式的单个特定实现的API很难成为标准。
  • WebSQL要求浏览器使用特定版本的SQLite(版本3.6.19),这意味着无论何时SQLite有任何更新或错误修复,都不可能在不破坏Web的情况下将其添加到WebSQL。
  • 像Firefox这样的主流浏览器从来不支持WebSQL。

因此,在下文中,我们将忽略WebSQL,即使可以通过设置特定的浏览器标志或使用旧版本的chromium来运行测试。


功能比较

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

存储复杂的JSON文档

当您在Web应用程序中存储数据时,通常您希望存储复杂的JSON文档,而不仅仅是存储在服务器端数据库中的integersstrings等"正常"值。

  • 只有IndexedDB可以原生地处理JSON对象。
  • 使用SQLite WASM,您可以将JSON存储在3.38.0(2022-02-22)版以来的text列中,甚至可以对其运行深度查询并使用单个属性作为索引。

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

多选项卡支持

ElectronReact-Native相比,构建Web应用程序的一个很大区别是,用户将同时在多个浏览器选项卡中打开和关闭应用程序。因此,您不仅有一个JavaScript进程在运行,而且有许多进程可以存在,并且可能必须在彼此之间共享状态更改,以免向用户显示过时的数据。

如果你的用户的肌肉记忆在使用你的网站时把左手放在F5键上,那么你做错了!

并非所有的存储API都支持在选项卡之间自动共享写入事件。

只有localstorage可以通过API本身和storage-event在选项卡之间自动共享写事件,storage-event可用于观察更改。

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

Chrome有实验性的IndexedDB观察者API,但提案存储库已经存档。

要解决此问题,有两种解决方案:

  • 第一个选项是使用BroadcastChannel API它可以在浏览器选项卡之间发送消息。因此,每当您对存储执行写入操作时,您也会向其他选项卡发送通知,以告知它们这些更改。这是RxDB也使用的最常见的解决方法。请注意,WebLocks API其可用于在浏览器选项卡之间具有互斥。
  • 另一种解决方案是使用SharedWorker,并在worker内部执行所有写入操作。然后,所有浏览器选项卡都可以订阅来自单个SharedWorker的消息,并了解更改。

索引支持

数据库和将数据存储在普通文件中的最大区别在于,数据库以允许在索引上运行操作的格式写入数据,以促进快速性能查询。在我们的技术列表中,只有IndexedDB和WASM SQLite支持开箱即用的索引。从理论上讲,你可以在任何存储(如localstorage或OPFS)上构建索引,但你可能不想自己这样做。

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

codeBlock_bY9V 复制代码
// 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主线程移开。这确保了我们的应用程序保持快速响应,同时处理可以在后台并行运行。在浏览器中,您可以使用WebWorkerSharedWorkerServiceWorkerAPI来完成此操作。在RxDB中,您可以使用WebWorkerSharedWorker插件将您的存储移动到worker中。

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

不幸的是,由于设计和安全限制,LocalStorage和Cookie不能在WebWorker或SharedWorker中使用。WebWorkers在与主浏览器线程不同的全局上下文中运行,因此不能做可能影响主线程的事情。他们无法直接访问某些Web API,如DOM、localStorage或Cookie。

其他一切都可以从WebWorker内部使用。带有createSyncAccessHandle方法的OPFS的快速版本只能在WebWorker中使用,而不能在主线程上使用。这是因为返回的AccessHandle的所有操作都不是JavaScript,因此会阻塞JavaScript进程,所以您确实希望在主线程上执行此操作并阻塞所有操作。


存储大小限制

  • Cookie仅限于RFC-6265中的4 KB数据。由于存储的cookie随每个HTTP请求发送到服务器,因此此限制是合理的。您可以在这里测试您的浏览器cookie限制。请注意,您不应该填写完整的4 KBcookie,因为您的Web服务器不会接受太长的标题,并拒绝使用HTTP ERROR 431 - Request header fields too large的请求。一旦你达到了这一点,你甚至不能提供更新的JavaScript给你的用户来清理cookie,你将锁定该用户,直到cookie得到手动清理。

  • LocalStorage的存储大小限制因浏览器而异,但通常每个源的范围为4 MB至10 MB。您可以在这里测试您的localStorage大小限制。

    • Chrome/Chromium/Edge:每个域5 MB
    • Firefox:每个域名10 MB
    • Safari:每个域4-5 MB(不同版本略有不同)
  • IndexedDB没有像localStorage那样的固定大小限制。IndexedDB的最大存储大小取决于浏览器实现。上限通常基于用户设备上的可用磁盘空间。在铬浏览器中,它可以使用高达80%的总磁盘空间。您可以通过调用await navigator.storage.estimate()获得关于存储大小限制的估计。通常,您可以存储千兆字节的数据,可以在这里尝试。

  • OPFS具有与IndexedDB相同的存储大小限制。其限制取决于可用的磁盘空间。这也可以在这里测试。


性能比较

现在我们已经回顾了每种存储方法的特性,让我们深入到性能比较,重点是初始化时间,读/写延迟和批量操作。

请注意,我们只运行简单的测试,对于您应用程序中的特定用例,结果可能会有所不同。此外,我们只比较性能在谷歌Chrome(版本128.0.6613.137)。Firefox和Safari具有相似但不相等的性能模式。你可以在你自己的机器上从这个github仓库运行测试。在所有的测试中,我们都将网络节流,使其表现得像德国的平均互联网速度。(下载:135,900 kbit/s,上传:28,400 kbit/s,延迟:125 ms)。此外,所有测试都存储一个"平均"JSON对象,根据存储情况,可能需要对该对象进行字符串化。我们也只测试通过id存储文档的性能,因为一些技术(cookie,OPFS和localstorage)不支持索引范围操作,所以比较这些技术的性能没有意义。

初始化时间

在存储任何数据之前,许多API都需要一个设置过程,例如创建数据库,生成WebAssembly进程或下载其他内容。为了确保应用快速启动,初始化时间非常重要。

localStorage和Cookie的API没有任何设置过程,可以直接使用。IndexedDB需要打开一个数据库和其中的存储。WASM SQLite需要下载一个WASM文件并处理它。OPFS需要下载并启动一个worker文件并初始化虚拟文件系统目录。

以下是从存储第一位数据所需的时间测量:

|------------------------|----------|
| 技术 | 时间毫秒 |
| IndexedDB | 46 |
| OPFS主线程 | 23 |
| OPFS WebWorker的使用 | 26.8 |
| WASM SQLite(内存) | 504 |
| WASM SQLite(IndexedDB) | 535 |

在这里我们可以注意到几件事:

  • 使用单个存储打开新的IndexedDB数据库需要花费惊人的时间
  • 从主线程向WebWorker OPFS发送数据的延迟开销大约为4毫秒。在这里,我们只发送最少的数据来初始化OPFS文件处理程序。当处理更多的数据时,如果延迟增加,那将是有趣的。
  • 下载和解析WASM SQLite并创建一个表大约需要半秒钟。使用IndexedDBVFS持久存储数据还额外增加了31毫秒。使用启用的缓存和已经准备好的表来访问页面要快一些,需要420毫秒(内存)。

小写入延迟

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

|------------------------|----------|
| 技术 | 时间毫秒 |
| Cookies | 0.058 |
| LocalStorage本地存储 | 0.017 |
| IndexedDB | 0.17 |
| OPFS主线程 | 1.46 |
| OPFS WebWorker的使用 | 1.54 |
| WASM SQLite(内存) | 0.17 |
| WASM SQLite(IndexedDB) | 3.17 |

在这里我们可以注意到几件事:

  • LocalStorage具有最低的写入延迟,每次写入仅为0.017毫秒。
  • IndexedDB的写入速度比localStorage慢10倍。
  • 将数据发送到WASM SQLite进程并让其通过IndexedDB持久化是很慢的,每次写入超过3毫秒。

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

小读取延迟

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

|------------------------|----------|
| 技术 | 时间毫秒 |
| Cookies | 0.132 |
| LocalStorage本地存储 | 0.0052 |
| IndexedDB | 0.1 |
| OPFS主线程 | 1.28 |
| OPFS WebWorker的使用 | 1.41 |
| WASM SQLite(内存) | 0.45 |
| WASM SQLite(IndexedDB) | 2.93 |

在这里我们可以注意到几件事:

  • LocalStorage读取速度非常快,每次读取仅为0.0052毫秒。
  • 其他技术执行读取的速度与其写入延迟相似。

大批量写入

下一步,让我们一次对200个文档进行一些大批量操作。

|------------------------|----------|
| 技术 | 时间毫秒 |
| Cookies | 20.6 |
| LocalStorage本地存储 | 5.79 |
| IndexedDB | 13.41 |
| OPFS主线程 | 280 |
| OPFS WebWorker的使用 | 104 |
| WASM SQLite(内存) | 19.1 |
| WASM SQLite(IndexedDB) | 37.12 |

在这里我们可以注意到几件事:

  • 将数据发送到WebWorker并通过更快的OPFS API运行它的速度大约是原来的两倍。
  • 与单次写入延迟相比,WASM SQLite在批量操作上的性能更好。这是因为如果一次性完成而不是每个文档一次,则将数据发送到WASM并向后发送会更快。

大批量读取

现在让我们在批量请求中读取100个文档。

|------------------------|----------------|
| 技术 | 时间毫秒 |
| Cookies | 6.34 |
| LocalStorage本地存储 | 0.39 |
| IndexedDB | 4.99 |
| OPFS主线程 | 54.79 |
| OPFS WebWorker的使用 | 25.61 |
| WASM SQLite(内存) | 3.59 |
| WASM SQLite(IndexedDB) | 5.84 (35ms无缓存) |

在这里我们可以注意到几件事:

  • 在OPFS webworker中阅读许多文件的速度大约是较慢的主线程模式的两倍。
  • WASM SQLite速度惊人。进一步的检查表明,WASM SQLite进程将文档保存在内存中缓存,这改善了我们在对同一数据进行写入后直接读取时的延迟。当浏览器选项卡在写入和读取之间重新加载时,查找这100个文档大约需要35毫秒。

性能结论

  • LocalStorage确实很快,但请记住它有一些缺点:
    • 它会阻塞主JavaScript进程,因此不应用于大批量操作。
    • 只有键值赋值是可能的,当你需要对数据进行基于索引的范围查询时,你不能有效地使用它。
  • 与直接在主线程中使用OPFS相比,在WebWorker中使用createSyncAccessHandle()方法时,OPFS要快得多。
  • SQLite WASM可以很快,但你必须首先下载完整的二进制文件并启动它,这大约需要半秒钟。如果您的应用程序启动一次并使用很长一段时间,这可能根本不相关。但是对于在许多浏览器标签中打开和关闭多次的网络应用程序来说,这可能是一个问题。

可能改进

有多种可能的改进和性能改进来加速操作。

  • 对于 IndexedDB,我在这里列出了性能技巧的列表。例如,您可以在多个数据库和网络工作者之间进行分片或使用自定义索引策略。
  • OPFS 在每个文档写入一个文件时速度很慢。但您不必这样做,您可以像普通数据库一样将所有内容存储在单个文件中。这极大地提高了性能,就像使用 RxDB OPFS RxStorage所做的那样。
  • 您可以混合使用这些技术来同时针对多个场景进行优化。例如,在 RxDB 中,有localstorage 元优化器,它将初始元数据存储在 localstorage 中,并将"普通"文档存储在 IndexedDB 内。这缩短了初始启动时间,同时仍然以高效查询的方式存储文档。
  • RxDB中有内存映射存储插件,可以将数据直接映射到内存。将此与共享工作程序结合使用可以显着改善页面加载和查询时间。
  • 在存储数据之前对其进行压缩可能会提高某些存储的性能。
  • 通过分片在多个 WebWorker 之间分割工作可以利用用户设备的全部容量来提高性能。

在这里,您可以看到各种RxDB存储实现的性能比较,这可以更好地了解真实的性能:

未来改进

你正在2024年阅读这篇文章,但网络并没有停滞不前。有一个很好的机会,浏览器得到增强,以允许更快,更好的数据操作。

  • 目前还没有办法从WebAssembly进程内部直接访问持久存储。如果将来发生变化,在浏览器中运行SQLite(或类似的数据库)可能是最好的选择。
  • 在主线程和WebWorker之间发送数据很慢,但将来可能会得到改进。有一个很好的文章关于为什么postMessage()是缓慢的。
  • IndexedDB最近支持存储桶(仅限Chrome),这可能会提高性能
相关推荐
清酒伴风(面试准备中......)2 小时前
Redis使用场景-缓存-缓存穿透
java·数据库·redis·缓存·面试·实习
懵懵懂懂程序员2 小时前
Debezium Engine监听binlog实现缓存更新与业务解耦
java·spring boot·redis·缓存
爬山算法2 小时前
Tomcat(39)如何在Tomcat中配置SSL会话缓存?
缓存·tomcat·ssl
苹果电脑的鑫鑫5 小时前
uni-app写的微信小程序每次换账号登录时出现缓存上一个账号数据的问题
缓存·微信小程序·uni-app
CN.LG6 小时前
浅谈C#库之Memcached
数据库·缓存·memcached
秦怀20 小时前
从单机缓存到分布式缓存那些事
分布式·后端·缓存
冧轩在努力1 天前
redis事务
数据库·redis·缓存
Achou.Wang1 天前
Redis中如何使用lua脚本-即redis与lua的相互调用
redis·缓存·lua
alden_ygq1 天前
go clean -modcache命令清理缓存
开发语言·缓存·golang