HTML5 本地存储终极指南:localStorage vs IndexedDB + 离线缓存完整方案
别再把所有数据一股脑塞进 localStorage 了。这篇文章帮你彻底搞清楚三个问题:该用哪个存?怎么配合离线缓存?性能差多少?
一、先看结论:一张表选对存储方案
| 维度 | localStorage | IndexedDB |
|---|---|---|
| 容量 | 5--10 MB / 域名 | 数百 MB(可达磁盘 50%) |
| 数据类型 | 仅字符串,对象需 JSON 序列化 | 任意结构化数据 + Blob / ArrayBuffer |
| 操作方式 | 同步,阻塞主线程 | 异步,不阻塞 UI |
| 查询能力 | 只能按 key 取,无索引 | 支持索引、范围查询、游标遍历 |
| 事务 | ❌ 无 | ✅ ACID 事务 |
| 过期机制 | ❌ 永久(除非手动删) | ❌ 永久 |
| 跨标签页共享 | ✅ | ✅ |
| 兼容性 | IE8+ 全部支持 | Chrome/Firefox/Safari/Edge 均支持 |
一句话原则:能塞进一张纸条的数据用 localStorage,需要建数据库的用 IndexedDB。
二、性能实测:差距不是一点半点
| 操作 | 数据量 | localStorage 耗时 | IndexedDB 耗时 |
|---|---|---|---|
| 写入单条小记录 | 1 KB | 0.3--0.5 ms | 1.8--2.1 ms |
| 读取单条小记录 | 1 KB | 0.3 ms | 1.8 ms |
| 写入 1000 条(共 1 MB) | 1 MB | 1250 ms ⚠️ | 320 ms ✅ |
| 读取 1000 条(共 1 MB) | 1 MB | 980 ms ⚠️ | 210 ms ✅ |
| 条件查询 100 条 | --- | 手动 filter(全量读取) | 15 ms(索引精准定位) |
超过 1 MB 的 JSON 写入,localStorage 能让主线程卡顿 400 ms 以上;同样的数据,IndexedDB 用事务分片读取,耗时压在 20--60 ms,UI 完全不卡。
更致命的是 :localStorage 写入超限会静默失败或抛 QuotaExceededError,很难及时发现;IndexedDB 会主动触发 storagequotaexceeded 事件,你能感知到。
三、localStorage:简单到极致,也弱到极致
适合存什么?
- 用户主题偏好(
theme: 'dark') - 登录态标记、访客 ID
- 表单草稿(几条字段)
- 筛选条件(不超 500 字符)
核心 API
javascript
js
localStorage.setItem('key', 'value');
localStorage.getItem('key');
localStorage.removeItem('key');
localStorage.clear();
必须知道的坑
| 坑 | 详情 |
|---|---|
| 只能存字符串 | 对象必须 JSON.stringify(),读取时 JSON.parse() |
| 无搜索能力 | 查"2024年创建的未完成任务"?全量读出再 filter,数据量一大就崩 |
| 同步阻塞 | 存 2 MB JSON,getItem() 卡主线程 400 ms+ |
| 被误清风险高 | 用户勾"清除历史记录"时,localStorage 内容会被一并清除 |
| 无事务 | "扣款+记账+记录流水"三步,任何一步失败,数据就不一致 |
四、IndexedDB:真正意义上的前端数据库
适合存什么?
- 几百条用户收货地址
- 离线地图瓦片、本地日志文件
- 按时间/标签/状态筛选的笔记
- 缓存大量 API 响应集合
- 音视频二进制资源(Blob / ArrayBuffer)
核心流程:三步走
ini
js
// 1. 打开/创建数据库
const request = indexedDB.open('MyAppDB', 2);
// 2. 版本升级时建表、建索引
request.onupgradeneeded = (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains('notes')) {
const store = db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true });
store.createIndex('createdAt', 'createdAt', { unique: false });
store.createIndex('status', 'status', { unique: false });
}
};
// 3. 事务中增删改查
request.onsuccess = (e) => {
const db = e.target.result;
const tx = db.transaction('notes', 'readwrite');
const store = tx.objectStore('notes');
store.add({ title: 'Hello', status: 'draft', createdAt: Date.now() });
};
查询示例:按时间范围 + 状态筛选
ini
js
const tx = db.transaction('notes', 'readonly');
const store = tx.objectStore('notes');
const idx = store.index('createdAt');
const range = IDBKeyRange.bound(Date.now() - 7 * 86400000, Date.now()); // 过去7天
const request = idx.openCursor(range);
request.onsuccess = (e) => {
const cursor = e.target.result;
if (cursor && cursor.value.status === 'draft') {
console.log(cursor.value);
}
cursor?.continue();
};
localStorage 做同样的查询,需要把全部数据 JSON.parse() 出来再 filter(),性能差一个数量级。
推荐封装库
原生 API 确实底层,建议用 idb(轻量、Promise 化、TypeScript 友好),把上面 30 行代码压缩到 5 行:
javascript
js
import { openDB } from 'idb';
const db = await openDB('MyAppDB', 1, {
upgrade(db) {
db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true });
}
});
await db.add('notes', { title: 'Hello', status: 'draft' });
五、离线缓存完整方案:Service Worker + Cache API
localStorage 和 IndexedDB 解决的是数据持久化 ,离线缓存解决的是资源可访问。两者配合,才是完整的离线优先(PWA)方案。
⚠️ Application Cache(manifest 文件)已被 W3C 废弃,不要再用了。
架构分层
| 层级 | 方案 | 存什么 |
|---|---|---|
| UI 状态 | localStorage | 主题、语言、折叠状态 |
| 业务数据 | IndexedDB | 笔记、待办、离线草稿 |
| 网络资源 | Cache API + Service Worker | HTML/CSS/JS/图片/音视频 |
Step 1:注册 Service Worker
c
js
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW 注册成功', reg))
.catch(err => console.log('SW 注册失败', err));
}
Step 2:sw.js 实现缓存策略
ini
js
const CACHE_NAME = 'my-app-v1';
const urlsToCache = ['/', '/index.html', '/styles/main.css', '/scripts/app.js', '/images/logo.png'];
// 安装:预缓存核心资源
self.addEventListener('install', (e) => {
e.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(urlsToCache))
);
});
// 拦截请求:缓存优先,未命中再网络
self.addEventListener('fetch', (e) => {
e.respondWith(
caches.match(e.request).then(cached => {
if (cached) return cached;
return fetch(e.request).then(resp => {
if (!resp || resp.status !== 200) return resp;
const clone = resp.clone();
caches.open(CACHE_NAME).then(cache => cache.put(e.request, clone));
return resp;
});
})
);
});
// 更新机制:SW 版本号变更触发
self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
)
);
});
Step 3:网络恢复后数据同步
javascript
js
// sw.js
self.addEventListener('sync', (e) => {
if (e.tag === 'sync-pending') {
e.waitUntil(syncPendingData());
}
});
async function syncPendingData() {
const db = await openDatabase();
const pending = await db.getAll('pending');
for (const item of pending) {
try {
await fetch('/api/sync', { method: 'POST', body: JSON.stringify(item) });
await db.delete('pending', item.id);
} catch (err) {
console.error('同步失败:', err);
}
}
}
六、实战组合方案:一个真实项目怎么选
| 数据 | 存哪里 | 为什么 |
|---|---|---|
| 主题色、语言 | localStorage | 几个字节,同步读取不影响性能 |
| JWT access_token | localStorage + 短期过期 | 轻量、快速,配合刷新机制 |
| 100 条收货地址 | IndexedDB | 需要按省市筛选,localStorage 查不动 |
| 笔记/待办列表 | IndexedDB | 离线编辑 + 同步,必须事务保障 |
| 表单草稿 | localStorage | 几条字段,丢失也不心疼 |
| 离线地图瓦片 | IndexedDB(存 Blob)+ Cache API(存 URL 映射) | 二进制数据 localStorage 存不了 |
| HTML/CSS/JS/图片 | Cache API | SW 拦截,离线秒开 |
| 播放进度 | localStorage | 轻量状态,{url, time} 一行 JSON 搞定 |
七、最终建议
| 项目类型 | 推荐方案 |
|---|---|
| 周期短、团队经验有限、需求简单 | localStorage + SW 缓存 |
| PWA / 离线优先应用 / 中大型系统 | IndexedDB + Cache API + SW |
| 已有 localStorage 代码但数据激增 | 新模块用 IndexedDB,旧数据按需迁移,别全量重写 |
| 混合使用 | localStorage 存开关和小配置,IndexedDB 管主体业务,内存 Map 提热数据性能 |
记住:localStorage 是便利贴,IndexedDB 是数据库,Service Worker 是快递员。便利贴贴几张够用,几百张订单得建库,快递员负责让你断网也能收货。