2. 本地存储选型对比:IndexedDB、SQLite、文件系统
离线优先应用需要在客户端持久化数据,主流方案包括 IndexedDB(浏览器内置)、SQLite(原生 / 混合应用)及文件系统直接读写。本章从原理、API、读写性能与适用场景进行对比,帮助你在选型时做出权衡。
2.1 原理与架构差异
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-15ms | BEGIN+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 综合选型决策矩阵
| 维度 | IndexedDB | SQLite | 文件系统 |
|---|---|---|---|
| 平台支持 | 浏览器 / 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+ 定位读写。 - 通用策略:所有存储方案都建议将写入操作合并为批处理,避免单条高频写入;离线数据同步完成后主动关闭连接 / 释放文件描述符。