2. 本地存储选型对比:IndexedDB、SQLite、文件系统

离线优先应用需要在客户端持久化数据,主流方案包括 IndexedDB(浏览器内置)、SQLite(原生 / 混合应用)及文件系统直接读写。本章从原理、API、读写性能与适用场景进行对比,帮助你在选型时做出权衡。

2.1 原理与架构差异

本地存储方案对比架构 IndexedDB 浏览器 JavaScript 对象存储 (Object Store) 异步事务 无结构化查询 主键索引 适用:PWA、Web 离线 读:中等 | 写:中等 事务范围:Object Store SQLite Native / WebAssembly 关系型表 (SQL) 同步/异步 (取决于绑定) 复杂查询/JOIN B-Tree 索引 适用:移动/桌面离线 读:高 | 写:高 事务范围:整个DB 文件系统 Node.js fs / File API 二进制/文本流 同步/异步 (fs) 无查询引擎 无索引 适用:大文件/日志/离线 读:高(顺序) 写:中(随机低)

2.2 IndexedDB 实战与性能

2.2.1 基本操作

// 打开数据库
const request = indexedDB.open('tasks', 1);
request.onupgradeneeded = (event) => {
  const db = event.target.result;
  if (!db.objectStoreNames.contains('tasks')) {
    const store = db.createObjectStore('tasks', { keyPath: 'id', autoIncrement: true });
    store.createIndex('priority', 'priority', { unique: false });
  }
};

// 写入
function addTask(task) {
  const db = await idbConnect();
  const tx = db.transaction('tasks', 'readwrite');
  tx.objectStore('tasks').add(task);
  await tx.done;
}

// 读取(按索引)
function getTasksByPriority(priority) {
  const db = await idbConnect();
  const tx = db.transaction('tasks', 'readonly');
  const index = tx.objectStore('tasks').index('priority');
  return await index.getAll(priority);
}

2.2.2 读写性能特征

操作延迟(千人级记录)注意事项
单记录写入~1-5ms事务自动提交,不可跨 Store
批量写入(1000条)~30-80ms使用 loop + request.onsuccess
按主键读取~0.5-2ms直接 get
范围查询(100条)~5-15ms使用索引 + bound
避坑:IndexedDB 的事务作用域必须在同一个连接实例中,且不能跨 context(如 iframe)。大量并发事务可能导致 version change 错误,建议使用单一连接池。

2.3 SQLite 实战与性能

2.3.1 基本操作(以 better-sqlite3 为例)

const Database = require('better-sqlite3');
const db = new Database('./offline.db', { pragma: { journal_mode: 'WAL' } });

// 建表
db.exec(`
  CREATE TABLE IF NOT EXISTS tasks (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    priority INTEGER DEFAULT 0,
    created_at TEXT DEFAULT (datetime('now'))
  )
`);

// 插入
const insert = db.prepare('INSERT INTO tasks (title, priority) VALUES (?, ?)');
insert.run('完成选型文档', 2);

// 查询(带索引)
db.prepare('CREATE INDEX idx_priority ON tasks(priority)').run();
const rows = db.prepare('SELECT * FROM tasks WHERE priority > ? ORDER BY created_at LIMIT 100').all(1);

2.3.2 读写性能特征

操作延迟(十万级记录)优化建议
单行插入 (WAL)~0.1-0.5ms使用事务组批量插入
批量插入 10000 行~8-15msBEGIN+COMMIT 包裹
主键点查询~0.05-0.2ms开启缓存 (cache_size)
范围查询 + 排序~1-5ms复合索引覆盖列
性能技巧:在离线优先场景下,SQLite 的 WAL 模式几乎是必选项;使用 PRAGMA synchronous=NORMAL 平衡写入性能与安全性。

2.4 文件系统实战与性能

2.4.1 文件读写(Node.js 环境)

const fs = require('fs').promises;
const path = require('path');

const DATA_PATH = path.join(__dirname, 'data');

// 写入 JSON 文件
async function saveTasks(tasks) {
  await fs.writeFile(
    path.join(DATA_PATH, 'tasks.json'),
    JSON.stringify(tasks),
    'utf8'
  );
}

// 读取 + 解析
async function loadTasks() {
  const raw = await fs.readFile(path.join(DATA_PATH, 'tasks.json'), 'utf8');
  return JSON.parse(raw);
}

// 追加日志(大文件)
const logStream = fs.createWriteStream(path.join(DATA_PATH, 'events.log'), { flags: 'a' });
logStream.write(JSON.stringify({ event: 'sync', ts: Date.now() }) + '\n');

2.4.2 读写性能特征

操作延迟(10MB 文件)限制
顺序写入(追加)~0.05ms/行(缓冲区)需手动 flush
随机写入(修改中间段)~10-20ms必须重写整个文件
全文件读取~2-5ms内存占用 = 文件大小
部分读取(seek)~0.1ms支持 fd.read
典型适用场景:文件系统最适合存储不可变资源(图片、二进制 blob)、操作日志、以及需要频繁追加的序列化数据。对于结构化查询需求,应选用 IndexedDB 或 SQLite。

2.5 综合选型决策矩阵

维度IndexedDBSQLite文件系统
平台支持浏览器 / WebView原生 + WebAssembly所有(原生 / Node)
查询能力按索引查询完整 SQL无
事务完整性存储级别ACID无(需应用层保证)
读取性能 (10万条)中等 (~10ms)高 (~1ms)中 (取决于文件大小)
写入性能 (10万条)中等 (~50ms)高 (~5ms)中-高 (顺序写入)
内存占用低 (异步)低-中 (缓存)低 (流式)
离线推荐场景PWA 独立缓存移动/桌面 App大文件 / 二进制资源
选型建议:
  • 纯 Web 离线 PWA → IndexedDB(配合 Service Worker Cache API 缓存资源)。
  • Electron / React Native / Flutter 离线应用 → SQLite(性能、事务、查询全面)。
  • 需要存储大文件(>100MB) → 文件系统(直接写入磁盘,避免数据库膨胀)。
  • 混合需求 → 文件系统存储资源,SQLite 存储结构化元数据索引路径。

2.6 性能稳定性优化要点

  • IndexedDB 优化:使用 request.onsuccess 而非 await 时注意引擎事件循环;开启事务时尽量窄化作用域,避免长时间保持连接。
  • SQLite 优化:使用预备语句(prepared statement)重复执行;定期执行 PRAGMA optimize;对只读操作使用 assert_no_attach。
  • 文件系统优化:写操作使用缓冲流(如 fs.createWriteStream)而非反复 writeFile;对大文件使用 fs.open + 定位读写。
  • 通用策略:所有存储方案都建议将写入操作合并为批处理,避免单条高频写入;离线数据同步完成后主动关闭连接 / 释放文件描述符。