2. 本地持久化方案选型:LocalStorage、IndexedDB、SQLite 对比与实现

埋点数据在客户端缓存,必须应对页面关闭、网络中断、频繁写入等场景。HTML5 规范与移动端原生环境提供了三种主流存储方案:LocalStorage、IndexedDB 与 SQLite。本章从原理机制、读写性能、事务能力、容量上限四个维度展开对比,并给出每种方案的最小可工作实现。

2.1 各方案核心原理与适用边界

维度LocalStorageIndexedDBSQLite
存储模型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 性能与稳定性对比基准

操作LocalStorageIndexedDBSQLite
单条写入 1KB0.3–0.8 ms1–3 ms2–5 ms
批量写入 100 条30–80 ms(同步)5–15 ms(异步批量)10–20 ms(事务)
读取全部 1000 条2–5 ms5–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 图描述了从埋点写入到持久化的方案选择逻辑,帮助开发者根据带宽、写入频率、数据类型快速锁定技术路径。

埋点持久化方案选型决策 埋点事件到达 写入频率 > 50/s? 单条大小 > 10KB? 需要 SQL 聚合? IndexedDB LocalStorage SQLite 选型说明 ▶ 高频写入 + 中等数据 → IndexedDB(非阻塞、支持事务) ▶ 低频率 + 小数据 + 无查询需求 → LocalStorage(零依赖、极简) ▶ 复杂聚合 / 离线报表 / 跨 session 历史查询 → SQLite ▶ 混用策略:用 IndexedDB 作热存储,SQLite 作冷存储定期迁移

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 语句转移。