浏览器缓存之【结构化数据库与缓存】: IndexedDB、Cache storage 和 Storage buckets

〇、前言

前文已经介绍了*,基础键值存储 Local storage 和 Session storage,*那么本文再介绍另一新的类别:结构化数据库与缓存。

结构化数据库与缓存,涵盖 IndexedDB、Cache storage 和 Storage buckets。

  • IndexedDB 是一个事务型数据库 ,支持存储大量结构化数据,适合复杂应用如离线文档编辑器或媒体播放器,属于应用数据存储
  • Cache storage 主要用于 Service Worker 缓存网络资源 ,实现离线访问和快速加载,属于资源性能存储
  • Storage buckets 则是更底层的存储分区机制 ,允许开发者按策略管理存储空间,常用于大型应用的资源隔离与配额控制,属于资源性能存储

下面来详细介绍下。

一、什么是 IndexedDB?

1.1 简介

IndexedDB 是浏览器提供的客户端事务型 NoSQL 数据库,专为存储大量结构化数据而设计,支持异步操作、事务机制和索引查询,适用于构建离线优先的 Web 应用(如:PWA-Progressive Web App,渐进式 Web 应用)。

其核心价值在于突破 localStorage 的容量与性能限制,允许直接存储复杂对象、二进制数据(Blob),并通过索引实现高效查询,同时避免阻塞主线程

IndexedDB 是 W3C 推荐的浏览器原生客户端数据库标准,作为 WebSQL 被弃用后的主流替代方案。它属于 NoSQL 类型数据库,以对象仓库(Object Store)而非表格形式组织数据,数据以键值对形式存储,支持 JavaScript 对象、数组、Blob 等复杂类型,无需序列化。

若需简化开发,可使用封装库(如:Dexie.js、localForage),它们提供 Promise API 和自动降级支持,大幅降低原生 IndexedDB 的复杂度。实际项目中,应优先评估数据规模与查询需求------当数据量 >100KB 或需复杂查询时,IndexedDB 是比 localStorage 更可靠的选择。

与 localStorage/WebSQL 的关键区别:

对比维度 localStorage / WebSQL IndexedDB
容量上限 通常限制为 5MB,且同步操作易阻塞 UI 无固定上限(一般 ≥250MB,可达设备空闲磁盘的 50%~60%)
数据操作方式 仅支持字符串键值对,无索引和事务支持 通过事务机制保证数据一致性,并支持多字段索引加速查询
同步性 操作是同步的,大数据量时会导致界面卡顿 完全异步,通过事件或 Promise 执行,不阻塞主线程

1.2 关键特性

1)核心能力

事务支持:操作必须在事务中执行,支持 readonly 和 readwrite 模式。 事务具备原子性------任一操作失败则整个事务回滚,确保数据一致性。

索引优化查询:可为对象仓库的任意字段创建索引 (如:name、updated_at),避免全表扫描。时间范围查询需配合 IDBKeyRange.bound() 使用。

二进制数据存储:直接存储 Blob/File 对象(如:图片、视频),无需转换为 Base64 字符串,适用于离线资源缓存。

2)限制与约束

同源策略:数据库仅限创建它的同源页面访问 ,无法跨域读写。

版本控制机制:数据库通过整数版本号 管理结构变更。版本号必须为整数(如:2.1 会被取整为 2),升级时需触发 onupgradeneeded 事件迁移数据。

隐身模式行为:数据仅驻留在内存中,关闭窗口后自动清除,且配额通常 ≤120MB(常规模式可达数 GB)。

1.3 核心概念架构

1)核心组件

组件 说明
数据库 顶层容器,每个数据库有唯一名称和整数版本号。
对象仓库 类似 NoSQL 的"集合",存储键值对数据。必须指定主键路径(keyPath)。
索引 基于字段的快速查询通道,可设为唯一(unique: true)或非唯一。
事务 操作的原子单元,限定作用域(如:db.transaction(['users'], 'readwrite'))。

2)数据组织逻辑

主键设计:优先使用业务主键 (如:user_id、message_id)作为 keyPath,避免依赖自增 ID(autoIncrement)导致多端同步冲突

分域存储:按业务拆分对象仓库(如:user_settings、chat_messages),避免单表膨胀导致锁竞争。

1.4 基本操作:初始化、增、删、改、查、索引查询

复制代码
// 【初始化】
const request = indexedDB.open("MyDB", 1); // 名称+版本号
// 首次创建或升级版本时触发
request.onupgradeneeded = (event) => {
  const db = event.target.result;
  // 创建对象仓库(主键为 id)
  const store = db.createObjectStore("users", { keyPath: "id" });
  // 为name字段添加索引
  store.createIndex("name", "name", { unique: false });
};
request.onsuccess = (event) => {
  const db = event.target.result; // 获取数据库实例
};
// 关键点:版本升级逻辑必须在 onupgradeneeded 中完成,否则无法修改结构

const tx = db.transaction("users", "readwrite");
const store = tx.objectStore("users");
// 【添加数据 add】
const request = store.add({ id: 1, name: "Alice", age: 25 }); // 主键需唯一
request.onsuccess = () => console.log('数据添加成功');
request.onerror = (event) => console.error('数据添加失败:', event.target.errorCode);

// 【删除数据 delete】
const request = store.delete(id);
request.onsuccess = () => console.log('数据删除成功');
request.onerror = (event) => console.error('数据删除失败:', event.target.errorCode);

// 【修改】
const request = store.put({ id: 1, name: "Alice", age: 26 });
request.onsuccess = () => console.log('数据更新成功');
request.onerror = (event) => console.error('数据更新失败:', event.target.errorCode);

// 【简单查询 get】
const request = store.get(id);
request.onsuccess = (event) => {
  if (event.target.result) {
    console.log('查询结果:', event.target.result);
  } else {
    console.log('未找到 ID 为', id, '的数据');
  }
};
request.onerror = (event) => console.error('数据查询失败:', event.target.errorCode);

// 【索引查询】
const index = store.index("name");
// 查询所有 name = "Alice" 的记录
index.getAll("Alice").onsuccess = (e) => console.log(e.target.result);

除了 getAll(),IndexedDB 的索引对象还提供了其他几种常用的查询方法,以应对不同的业务场景:

方法 说明 返回值
index.get(value) 获取匹配该索引值的第一条记录 单个对象或 undefined
index.getAll(value) 获取匹配该索引值的所有记录 数组 []
index.getAllKeys(value) 获取匹配该索引值的所有主键 主键数组 []
index.count(value) 统计匹配该索引值的记录数量 数字 Number

注意:

事务上下文:在执行 store.get() 或 store.index() 之前,请确保当前处于一个有效的 readwrite 或 readonly 事务(Transaction)中。如果事务已经结束或发生错误,调用这些方法会抛出异常。

索引必须提前创建:store.index("name") 中的 "name" 必须在数据库初始化(onupgradeneeded)时通过 objectStore.createIndex('name', 'name', { unique: false }) 创建过,否则会报错。

分批提交:单次事务写入 500~2000 条,避免长时间阻塞。

主动让出主线程:用 queueMicrotask 或 setTimeout 间隔执行批次。

1.5 典型应用场景

1)离线优先应用(PWA)

离线数据持久化:用户操作实时存入 IndexedDB,网络恢复后同步至服务器。

配合 Service Worker:缓存静态资源(Cache API),IndexedDB 存储动态数据,实现完整离线体验。

2)大数据量客户端缓存

结构化数据缓存:商品目录、用户历史记录等 >100KB 的数据,避免频繁请求服务器。

性能敏感场景:对比测试显示,2 万条商品数据(约 4.8MB)的读写,IndexedDB 比 localStorage 快 3.8~6.3 倍,索引查询效率提升 ≥16 倍。

3)二进制资源管理

图片/文档本地缓存:存储 Blob 数据,实现"秒开"体验(如文档查看器、图片库)。

游戏状态保存:关卡进度、玩家属性等需结构化存储的场景。

1.6 使用注意事项与最佳实践

1)工程化设计原则

按业务域拆分仓库:避免将用户配置、日志、临时数据混存于同一对象仓库。

索引精简化:仅高频查询字段建索引,长文本字段(如:content)不适合索引,全文检索应交由专用库(如:FlexSearch)。

2)容量与清理策略

主动监控配额:通过 navigator.storage.estimate() 检测用量,当 usage/quota > 0.8 时触发清理。

分级清理机制:优先删除带 expiresAt 的临时数据(如:日志),再清理旧快照,避免误删核心数据。

3)隐私与安全

敏感数据加密:IndexedDB 本身 无内置加密,存储密码等信息需先通过 Web Crypto API 加密。

隐身模式限制:数据仅内存驻留,关闭窗口即销毁,不可依赖其持久化能力。

二、什么是 Cache storage?

2.1 简介

Cache Storage 是浏览器提供的专用于缓存网络请求/响应对(Request/Response)的持久化存储机制,由开发者通过 JavaScript 显式控制,独立于 HTTP 缓存头,主要用于实现离线优先的 Web 应用(如:PWA)。

其核心价值在于完全自主管理资源缓存策略(如:离线访问、网络回退),而非依赖服务器设置的缓存规则。

Cache Storage 将缓存控制权交给开发者,使其能精准实现离线场景需求

实际使用时,应避免将其视为"自动缓存"工具------所有缓存操作必须显式编码,且需严格管理生命周期

对于简单场景,可结合 workbox 等库简化策略实现;若仅需缓存静态资源,优先通过 HTTP 头配置强缓存,而非过度依赖 Cache Storage。

Cache Storage 是 W3C Service Workers 规范的一部分,但 不限于 Service Worker 环境,也可在主线程通过 caches 全局对象访问。它专为存储HTTP 请求-响应对设计,直接缓存二进制资源(如:HTML、CSS、JS、图片),而非结构化数据(后者应使用 IndexedDB)。

Cache Storage 与 HTTP 缓存的关键区别:

对比维度 HTTP 缓存 Cache Storage
控制权归属 由服务器通过 Cache-Control / Expires 响应头控制。 完全由开发者通过代码管理,忽略 HTTP 缓存头。
缓存粒度 基于 URL 和响应头自动生效。 需显式调用 API 添加/匹配资源,支持自定义匹配逻辑(如:忽略查询参数)。
适用场景 适用于常规页面加载加速。 专为离线场景设计(如:PWA 的离线资源预加载)。

2.2 核心特性与限制

1)关键能力

持久化存储:缓存数据跨页面刷新和浏览器会话保留,除非开发者主动删除

独立于网络请求 :可通过 caches.match(request) 手动检查缓存,无需实际发起网络请求。

灵活匹配策略:支持自定义匹配逻辑(如:忽略 URL 查询参数),通过 Cache.match(request, options) 的 options 参数控制。

2)重要限制

仅支持 GET 请求 :Cache.add()/addAll()/put() 无法缓存 POST 等非安全请求。

HTTPS 强制要求:生产环境中必须通过 HTTPS 服务 (本地开发可使用 http://localhost),否则 API 调用会抛出 SecurityError。

存储空间受配额约束:浏览器对单域名缓存总量有上限(通常为设备空闲磁盘的 50%~60%),需通过 navigator.storage.estimate() 监控用量。

2.3 核心 API 与工作流程

基础操作方法有:

方法 作用
caches.open(cacheName) 打开指定名称的缓存仓库,若不存在则自动创建。
cache.addAll(urls) 批量缓存资源列表,任一失败则全部回滚。
cache.put(request, response) 手动存入请求-响应对(常用于动态缓存)。
caches.match(request) 跨所有缓存仓库匹配请求,返回首个匹配的响应。
cache.keys() 获取当前缓存仓库中所有请求的列表。

典型工作流程(Service Worker 场景)

1)安装阶段预缓存资源

复制代码
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('v1').then(cache => 
      cache.addAll(['/index.html', '/style.css', '/app.js'])
    )
  );
});
// 关键点:通过 waitUntil 确保缓存完成前 Service Worker 不进入激活状态

2)拦截请求并返回缓存

复制代码
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then(cachedResponse => 
      cachedResponse || fetch(event.request) // 无缓存则走网络
    )
  );
});
// 扩展策略:可实现 cache-first(优先缓存)、network-first(优先网络)等混合策略

2.4 典型应用场景

1)PWA 离线资源管理

预加载核心资源 :在安装阶段缓存 Shell(HTML/CSS/JS),确保 首次加载后完全离线可用。

动态资源缓存:用户访问时缓存图片/数据,后续离线时仍可查看历史内容。

2)自定义缓存策略

忽略查询参数 :通过 new Request(url, { ignoreSearch: true }) 匹配忽略 ?v=1.2.3 的资源。

版本化缓存:按版本号命名缓存仓库(如:app-v2),旧版本缓存需手动清理,避免空间浪费。

3)网络优化

降级体验保障:网络请求失败时返回缓存内容 (stale-while-revalidate 模式)。

减少重复请求:对静态资源 (如:字体、第三方库)实现跨页面/会话的持久缓存

2.5 注意事项和最佳实践

1)缓存管理规范

必须主动清理旧缓存:Service Worker 更新时,旧缓存不会自动删除,需在 activate 事件中清理:

复制代码
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then(keys => 
      Promise.all(keys.filter(k => !['v1','v2'].includes(k)).map(k => caches.delete(k)))
    )
  );
});

避免缓存污染:仅缓存明确需要离线支持的资源,避免无差别缓存导致配额耗尽

2)安全与兼容性

HTTPS 强制约束:非安全上下文(HTTP)中,Chrome 等浏览器完全禁用 Cache API (本地开发除外)。

隐身模式行为:数据仅驻留在内存中,关闭窗口后自动清除,且配额通常 ≤120MB。

3)与 HTTP 缓存协同

双重缓存风险:若资源同时被 HTTP 缓存和 Cache Storage 存储,浏览器会优先使用 HTTP 缓存(因更接近网络层)

合理设置 HTTP 头:对 Cache Storage 中的资源,建议设置 Cache-Control: max-age=0,避免 HTTP 缓存干扰版本更新

三、什么是 Storage buckets?

注意:截至目前,Storage Buckets API 仍处于 WICG 提案草案阶段,尚未在任何主流浏览器中实现。这意味着它目前无法在生产环境中直接调用,仅供了解其思路。

Storage Buckets(存储桶)是 Web 存储技术中一个极具前瞻性的全新存储管理概念。它的核心设计目标是允许开发者将不同类型的数据分组到不同的"存储桶"中,并为每个桶配置独立的配额、持久化策略和清理规则

在传统的 Web 存储方式中,一个域名(Origin)下的所有数据(如:localStorage、IndexedDB、Cache API 等)通常混在一起,共享浏览器的总存储配额。这导致了两个主要痛点:配额竞争和清理策略一刀切 。Storage Buckets 通过将数据分类到独立的"桶"中,完美解决了上述问题。每个存储桶提供了精细化的控制能力:独立配额管理(Quota)、持久化策略(Persisted)、耐用性级别(Durability)。

然而,在深入了解其强大能力之前,必须明确一个关键的现状:截至目前,Storage Buckets API 仍处于 WICG 提案草案阶段,尚未在任何主流浏览器中实现。 这意味着它目前无法在生产环境中直接调用,现阶段无法依靠标准 Web API 实现模块级的配额硬隔离,浏览器也不提供按模块切分配额的粒度,但它是未来 Web 存储精细化管理的重要发展方向。

如果当前确实需要实现类似"模块数据隔离"的效果,建议采用以下替代策略:

模拟隔离:使用 IndexedDB 创建独立的数据库(如:module-a-db),并在键名中强制加上模块标识前缀,通过封装读写函数来避免跨模块误读。

物理隔离:将模块拆分为不同的子域名(如:auth.example.commedia.example.com),利用 origin 的天然隔离获得独立的存储配额。

服务端托管:由后端按模块制定存储配额策略(如:限制 Redis 内存),前端仅存储临时句柄或 token。

Storage Buckets 描绘了 Web 存储精细化管理的未来蓝图。虽然目前它还只是一个"名字很好听的幻影",但前端开发者可以提前了解其分层存储的设计思想,并在当前架构中通过命名空间封装等方式做好数据隔离的准备工作,以便在未来 API 落地时能够无缝迁移。

四、IndexedDB 和 Cache Storage 两者的对比

在构建现代 Web 应用(尤其是 PWA 和离线优先应用)时,IndexedDB 和 Cache Storage 是两种最核心的本地存储机制。虽然它们都支持大容量存储和异步操作,但其设计初衷和适用场景截然不同。

注:渐进式 Web 应用(PWA)是一种使用现代 Web 技术(HTML、CSS、JavaScript)构建,但能提供类似原生应用体验的 Web 应用程序。其架构设计的核心目标是实现跨平台兼容、离线可用、快速加载和原生级交互。

  • 三个核心区别:

1)设计初衷与存储对象不同

IndexedDB 是浏览器内置的完整 NoSQL 数据库。它主要用于存储结构化的业务数据(如:JSON 对象、聊天记录、用户文档、配置信息等)。它支持复杂的数据模型、多字段索引以及事务(ACID),非常适合需要频繁增删改查(CRUD)和条件过滤的场景。

Cache Storage(缓存存储 API)是一个专门用于存储 HTTP 响应的映射表。它主要用于存储静态资源文件(如 HTML、CSS、JavaScript、图片等)。它通常与 Service Worker 配合使用,通过拦截 fetch 请求来实现离线加载和页面秒开。

2)数据查询与操作方式不同

IndexedDB 具备强大的查询能力。开发者可以创建索引,使用游标(Cursor)和键范围(Key Range)进行复杂的数据检索、排序和过滤。

Cache Storage 本质上是一个键值对存储(Key-Value Store),其"键"是 Request(请求),"值"是 Response(响应)。它没有索引功能,仅能通过精确的 URL 或自定义的 match 逻辑(如:忽略查询参数)来匹配资源。

3)底层存储引擎不同

IndexedDB 在用户磁盘上使用了真实的数据库引擎。例如,基于 Chromium 的浏览器(Chrome、Edge)使用 LevelDB(LSM 树),而 Firefox 和 Safari 使用 SQLite(B树),这保证了其在处理大型数据集时的高性能和持久性。

Cache Storage 则更偏向于文件系统的响应映射,专门针对网络请求的缓存与驱逐策略进行了优化。

核心特性对比维度 IndexedDB Cache Storage
存储内容 结构化对象、JSON、二进制文件(Blob) 完整的 HTTP 响应(Request/Response 对)
主要用途 离线业务数据、复杂应用状态、用户消息 静态资源(JS/CSS/图片)、离线应用外壳(HTML)
查询能力 强大(支持索引、键范围查询、游标遍历) 较弱(仅能通过 URL 或自定义规则进行匹配)
API 类型 事务性、对象存储、异步回调/Promise 基于 Promise 的请求/响应映射
存储容量 非常大(通常可达设备空闲磁盘的 50%~60%) 非常大(受浏览器整体配额管理限制)
典型缓存策略 数据级同步(如后台同步队列、增量保存) 网络级拦截(缓存优先、网络优先、SWR等)

在实际的 PWA 架构中,这两者通常不是"二选一"的关系,而是协同工作:

  • Service Worker 作为协调器拦截请求。
  • 当请求是 /styles.css 或 /app.js 时,Service Worker 从 Cache Storage 中返回资源,确保应用 UI 瞬间加载。
  • 当请求是 /api/user-data 时,Service Worker 可能会从 IndexedDB 中读取上次缓存的业务数据返回给用户,同时在后台发起网络请求更新 IndexedDB 中的最新数据。

五、小小的总结

本文系统梳理了现代 Web 应用的三大核心存储支柱,旨在帮助开发者根据具体场景选择最合适的存储策略:

  1. IndexedDB(结构化数据库)
    • 定位 :浏览器内的事务型 NoSQL 数据库。
    • 适用 :存储大量结构化数据(如用户文档、聊天记录、离线业务数据)。
    • 特点 :容量大(可达 GB 级)、支持索引查询、事务安全、异步不阻塞 UI。
  2. Cache Storage(资源缓存)
    • 定位 :专用于缓存网络请求/响应对(Request/Response)。
    • 适用 :实现 PWA 离线访问、静态资源(HTML/CSS/JS/图片)预加载、自定义网络策略。
    • 特点 :由开发者代码完全控制、独立于 HTTP 缓存头、通常与 Service Worker 配合使用。
  3. Storage Buckets(存储分区 - 提案阶段)
    • 定位 :未来的存储配额与策略管理机制。
    • 现状 :目前仍处于 WICG 提案草案阶段,主流浏览器尚未实现。
    • 价值 :旨在解决配额竞争和清理策略"一刀切"的问题,允许按桶(Bucket)进行精细化管理。

最终建议

  • 存数据用 IndexedDB :当需要存储复杂的业务对象且需要检索时。
  • 存资源用 Cache API :当需要拦截网络请求、实现离线包或加速静态资源加载时。
  • 关注未来 Buckets :虽然目前无法直接使用,但在架构设计上应提前考虑数据隔离的思想。
相关推荐
user20585561518131 小时前
X6 中边悬浮置顶,规避 `mouseleave` 事件丢失问题
前端
李明卫杭州1 小时前
CSS aspect-ratio 属性完全指南
前端
Pedantic3 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端
飘尘3 小时前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆4 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师5 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
雨季mo浅忆5 小时前
VSCode自动格式化三要素
前端
爱勇宝5 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员