2. 本地持久化方案选型:LocalStorage、IndexedDB、SQLite 对比与实现
埋点数据在客户端缓存,必须应对页面关闭、网络中断、频繁写入等场景。HTML5 规范与移动端原生环境提供了三种主流存储方案:LocalStorage、IndexedDB 与 SQLite。本章从原理机制、读写性能、事务能力、容量上限四个维度展开对比,并给出每种方案的最小可工作实现。
2.1 各方案核心原理与适用边界
| 维度 | LocalStorage | IndexedDB | SQLite |
|---|---|---|---|
| 存储模型 | Key-Value 字符串对 | 对象仓库 / 索引 | 关系型表 / SQL 查询 |
| 数据类型 | 仅字符串(需 JSON.stringify) | 结构化克隆(对象、Blob、ArrayBuffer) | 标准 SQL 类型(TEXT / INTEGER / BLOB) |
| 事务支持 | 无 | 读写事务(自动提交) | 完整 ACID 事务 |
| 容量上限 | 通常 5~10 MB | 通常 ≥ 50 MB(取决于用户磁盘) | 无硬上限(受限于文件系统) |
| 查询能力 | 仅按 key 精确匹配 | 游标 + 索引范围查询 | SQL JOIN / GROUP BY / 子查询 |
| 适用场景 | 少量配置 / 标记位 | 中高频写入 + 批量读取 | 复杂聚合 / 离线分析 / 跨 session 查询 |
核心选型判断:LocalStorage 适合 < 100 条/秒的简单缓存;IndexedDB 适合浏览器端大量结构化埋点;SQLite 适合 Hybrid 或原生应用中对数据完整性要求极高的离线链路。
2.2 各方案最小实现流程与代码
2.2.1 LocalStorage 写入/读取/清除
// 写入埋点缓存(自动序列化)
function appendToLS(event) {
const KEY = 'track_events';
let stored = localStorage.getItem(KEY);
let events = stored ? JSON.parse(stored) : [];
events.push(event);
// 检查大小(估算)
const approxSize = new Blob([JSON.stringify(events)]).size;
if (approxSize > 4 * 1024 * 1024) {
events.shift(); // 丢弃最旧事件
}
localStorage.setItem(KEY, JSON.stringify(events));
}
// 读取并清空(用于上报)
function popAllFromLS() {
const KEY = 'track_events';
const raw = localStorage.getItem(KEY);
if (!raw) return [];
localStorage.removeItem(KEY);
return JSON.parse(raw);
}
2.2.2 IndexedDB 数据库创建与批量写入
const DB_NAME = 'TrackDB';
const STORE_NAME = 'events';
const DB_VERSION = 1;
function openDB() {
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, DB_VERSION);
req.onupgradeneeded = (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME, {
keyPath: 'id',
autoIncrement: true
});
}
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
async function batchWriteToIDB(events) {
const db = await openDB();
const tx = db.transaction(STORE_NAME, 'readwrite');
const store = tx.objectStore(STORE_NAME);
for (const evt of events) {
store.add(evt);
}
await new Promise((resolve, reject) => {
tx.oncomplete = resolve;
tx.onerror = () => reject(tx.error);
});
db.close();
}
2.2.3 SQLite(通过 WebSQL / 原生插件)建表与插入
浏览器端 WebSQL 已被废弃,本示例使用 Cordova/Electron 下的 cordova-sqlite-storage 插件 API。
// 打开数据库
const db = window.sqlitePlugin.openDatabase({
name: 'track.db',
location: 'default'
});
// 建表(仅首次)
db.executeSql(`
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_name TEXT NOT NULL,
payload TEXT,
created_at INTEGER NOT NULL
)
`, [], () => console.log('Table ready'), (err) => console.error(err));
// 批量插入埋点
function batchInsertSQLite(events) {
db.sqlBatch(events.map(evt => [
'INSERT INTO events (event_name, payload, created_at) VALUES (?,?,?)',
[evt.name, JSON.stringify(evt.data), Date.now()]
]), () => console.log('Inserted'), (err) => console.error(err));
}
2.3 性能与稳定性对比基准
| 操作 | LocalStorage | IndexedDB | SQLite |
|---|---|---|---|
| 单条写入 1KB | 0.3–0.8 ms | 1–3 ms | 2–5 ms |
| 批量写入 100 条 | 30–80 ms(同步) | 5–15 ms(异步批量) | 10–20 ms(事务) |
| 读取全部 1000 条 | 2–5 ms | 5–15 ms(游标) | 3–8 ms(SELECT) |
| 并发写入 | 阻塞 UI 线程 | 非阻塞,支持并行事务 | 非阻塞(WAL 模式) |
| 崩溃后一致性 | 无事务保护,可能丢失最后写入 | 事务内原子提交 | ACID 确保完整恢复 |
实测建议:在端内埋点 SDK 中,用 IndexedDB 作为主缓存层,LocalStorage 仅用于存储 SDK 配置 ;SQLite 只在需要离线聚合报表或跨分页查询时启用。
2.4 调试与边界检查
- LocalStorage:打开 Chrome DevTools → Application → Local Storage,观察键值对大小;使用
Blob.size或new Blob([value]).size估算占用,避免超过单域配额触发QuotaExceededError。 - IndexedDB:DevTools → Application → IndexedDB 可查看对象仓库内容和索引;监控
versionchange事件防止多 Tab 冲突。 - SQLite:通过
PRAGMA page_count和PRAGMA page_size估算数据库文件大小;开启 WAL 模式降低锁冲突。
常见错误—存储撑爆:埋点事件在无网络时持续累积,LocalStorage 会在写满时直接抛异常;IndexedDB 和 SQLite 会缓慢增大,最终导致应用闪退。请始终在写入前判断剩余容量,并启用 FIFO 淘汰策略。
2.5 一次完整的选型决策
以下 SVG 图描述了从埋点写入到持久化的方案选择逻辑,帮助开发者根据带宽、写入频率、数据类型快速锁定技术路径。
2.6 性能优化与未来迁移
- 内存中转:高频写入时先在内存 Map 中累积,每 5 秒或积攒 50 条批量写入一次存储,减少 I/O 次数。
- 容量预警:每次写入后检查存储百分比(IndexedDB 通过
navigator.storage.estimate()),超过 80% 时触发 FIFO 清理。 - 多 Tab 同步:IndexedDB 通过
versionchange事件监听其他 tab 的 schema 变更;LocalStorage 可使用storage事件做跨 tab 通知。 - 升级迁移:从 LocalStorage 迁移到 IndexedDB 时可一次性读取全量数据,写入新库后删除旧 key;SQLite 迁移建议使用 ATTACH DATABASE 语句转移。