导语:数据库跑进浏览器与边缘
浏览器应用越来越需要「本地数据库」:离线优先、端到端低延迟、数据不出设备。传统答案 localStorage(字符串 KV)容量小、无查询;IndexedDB(对象仓库)有查询但复杂。WASM 给出了最强答案——把 SQLite 整个编译进浏览器,一个完整的 SQL 数据库直接跑在用户设备上。本文系统讲 WASM 数据库与持久化:为什么是 SQLite、sql.js/wa-sqlite/libsql 三库对比、编译加载、OPFS/IndexedDB 持久化方案、WASI 下的服务端场景、离线同步策略与性能基准。
前置:/wasm-wasi-filesystem-sandbox/(文件系统沙箱)、/wasm-embedding-hosting-apis/(嵌入 API)、/wasm-multithreading-sharedarraybuffer/(多线程)。
目录
- 1. WASM 里的数据库:为什么是 SQLite
- 2. sql.js、wa-sqlite 与 libsql:三库对比
- 3. 编译与加载:把 SQLite 编成 WASM
- 4. 浏览器持久化方案:OPFS、IndexedDB 与私有文件系统
- 5. OPFS 详解:同步访问与导入导出
- 6. WASI 下的 SQLite:服务端持久化
- 7. 离线应用与数据同步策略
- 8. 性能:WASM SQLite 能跑多快
- 9. 选型矩阵与生产建议
- 10. 速查表与一句话记忆
- 延伸阅读
1. WASM 里的数据库:为什么是 SQLite
浏览器存储能力对比,SQLite 的定位一目了然:
| 方案 | 容量 | 查询 | 复杂度 | 持久化 |
|---|---|---|---|---|
| localStorage | ~5MiB | 无 | 极低 | 同步 API |
| IndexedDB | 数百 MiB~GB | 索引/游标 | 中 | 事务 |
| WASM SQLite | 数 GB(受 OPFS 配额) | 完整 SQL | 中高 | 文件/OPFS |
SQLite 的三个独特优势:完整 SQL(JOIN、事务、索引、全文检索 FTS5)、单文件(.db 文件可导出/备份/同步)、久经考验(嵌入式数据库之王,沙箱内零服务进程)。WASM 让它在浏览器里「原生态跑」——用官方 SQLite 源码编译,无转译偏差。
一句话总结:WASM SQLite = 浏览器里的完整 SQL + 单文件 + 零服务进程,补齐 localStorage 无查询、IndexedDB 复杂的短板。
2. sql.js、wa-sqlite 与 libsql:三库对比
| 库 | 构建方式 | 持久化 | 特点 |
|---|---|---|---|
| sql.js | Emscripten | 手动导出/导入 Uint8Array | 经典方案,内存中跑,无原生持久化 |
| wa-sqlite | Emscripten + OPFS | OPFS 原生句柄 | 支持 WAL 模式、同步访问句柄,性能最佳 |
| libsql | C 编译 | 本地 + 远程同步 | Turso 的 SQLite fork,支持嵌入式副本与远程同步 |
三库的持久化差异决定选型:
□ sql.js:每次操作后 export() 成 Uint8Array 存 IndexedDB,全量写
□ wa-sqlite:直接把 OPFS 文件句柄给 SQLite,增量写 WAL
□ libsql:本地主副本 + 远程副本双向同步,离线优先
→ sql.js 适合小数据/临时,wa-sqlite 适合正式离线应用,
libsql 适合需要跨设备/服务端同步的应用
一句话总结:sql.js 是内存兜底、wa-sqlite 是 OPFS 原生持久化、libsql 是带同步的 fork——持久化需求决定选哪家。
3. 编译与加载:把 SQLite 编成 WASM
3.1 用 Emscripten 编译
# 从 sqlite 官方 amalgamation 编译(裁剪扩展减小体积)
emcc sqlite3.c -o sqlite3.js sqlite3.wasm \
-s WASM=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s EXPORTED_FUNCTIONS=_sqlite3_open,_sqlite3_prepare_v2,_sqlite3_step,_sqlite3_exec \
-DSQLITE_OMIT_LOAD_EXTENSION \
-DSQLITE_THREADSAFE=0
3.2 加载策略
import initSqlJs from 'sql.js';
const SQL = await initSqlJs({
locateFile: (f) => `https://cdn.jsdelivr.net/npm/sql.js@1.10/dist/${f}`,
});
const db = new SQL.Database(); // 内存库
db.run('CREATE TABLE t(id INTEGER PRIMARY KEY, v TEXT)');
db.run('INSERT INTO t VALUES (1, "hello")');
const result = db.exec('SELECT * FROM t'); // 标准 exec API
编译要点:ALLOW_MEMORY_GROWTH=1 允许线性内存增长;SQLITE_THREADSAFE=0 单线程省锁开销;用 SQLITE_OMIT_* 宏裁剪不需要的特性减体积。
一句话总结:编译 SQLite =
emcc一条命令 + 若干裁剪宏;加载走initSqlJs,内存库开箱即用,持久化要另配。
4. 浏览器持久化方案:OPFS、IndexedDB 与私有文件系统
4.1 三种持久化载体
| 载体 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| IndexedDB | 存 .db 快照(Blob) | 兼容最广 | 全量读写、无增量 |
| OPFS | 浏览器私有文件系统 | 原生文件句柄、增量写、性能好 | 仅 Chrome 系 + 需 async handle |
| Cache API | 缓存 .db 快照 | 简单 | 不适合频繁写 |
4.2 私有文件系统(Private File System)
OPFS(Origin Private File System)是 File System Access API 的「私有」形态:每个站点有自己的隔离文件系统,不出 origin 不可见,天然贴合沙箱模型。SQLite 通过 FileSystemSyncAccessHandle 获得类 POSIX 的同步读写。
const root = await navigator.storage.getDirectory(); // OPFS 根
const fh = await root.getFileHandle('app.db', { create: true });
const access = await fh.createSyncAccessHandle(); // 同步句柄
// 把 handle 传给 wa-sqlite,SQLite 直接读写该文件
一句话总结:IndexedDB 存快照、OPFS 给原生句柄——追求性能与增量写选 OPFS,追求兼容与简单选 IndexedDB。
5. OPFS 详解:同步访问与导入导出
5.1 同步访问句柄
普通 FileSystemFileHandle 的读写是异步的,但 createSyncAccessHandle() 返回同步句柄——可以放进 SQLite 的 VFS 做真正的位置偏移读写,WAL 模式下增量落盘:
// wa-sqlite 的 OPFS 适配器(示意)
import { OPFSDatabase } from '@wa-sqlite/driver-opfs-sahpool';
const db = await new OPFSDatabase('app.db', {
durability: 'relaxed', // relaxed = WAL 异步 fsync,性能优先
});
await db.exec('CREATE TABLE IF NOT EXISTS kv(k TEXT PRIMARY KEY, v TEXT)');
await db.exec("INSERT INTO kv VALUES('answer', '42')");
5.2 导入与导出
OPFS 与外部世界隔离,数据进出要显式搬运:
// 导出:OPFS 文件 → Blob → 下载/同步
const data = await root.getFileHandle('app.db', {}).then(h => h.getFile());
const blob = await data.slice(); // Blob
// 导入:上传的 .db 文件 → OPFS
const writeAccess = await root.getFileHandle('app.db', { create: true })
.then(h => h.createWritable());
await writeAccess.write(await uploadedFile.arrayBuffer());
await writeAccess.close();
一句话总结:OPFS 用同步访问句柄给 SQLite 增量写,导入导出靠 Blob/Writable 显式搬运——
durability: 'relaxed'换性能、'strict'换安全。
6. WASI 下的 SQLite:服务端持久化
同一份 SQLite 源码编译到 WASI 目标,就能在 Wasmtime/边缘运行时跑,直接映射宿主目录:
# 编译 WASI 目标
clang --target=wasm32-wasi -O2 -DSQLITE_THREADSAFE=0 \
sqlite3.c shell.c -o sqlite3.wasm -lsqlite3
# Wasmtime 挂载目录运行
wasmtime run --dir /data::/data sqlite3.wasm \
/data/app.db "SELECT * FROM t;"
服务端场景:边缘 Worker/Serverless 函数用 WASM SQLite 做本地热数据缓存,配合上游数据库;--dir /data::/data 只暴露数据目录,其余文件系统不可见,契合最小权限。
一句话总结:WASI SQLite = 同一份代码跑服务器/边缘,
wasmtime run --dir挂目录即用,适合边缘本地缓存与离线批处理。
7. 离线应用与数据同步策略
7.1 离线优先架构
读路径:WASM SQLite(本地)→ 命中返回 → 未命中查上游并写缓存
写路径:本地事务落盘 → 变更日志(changelog)→ 网络恢复后批量同步
冲突:LWW(last-write-wins,带版本号)或 CRDT(多端编辑)
7.2 变更追踪与同步
-- 本地加一张变更表,记录每个改动
CREATE TABLE changes(
id INTEGER PRIMARY KEY,
table_name TEXT, row_id INTEGER,
op TEXT, ts INTEGER, -- upsert / delete
payload BLOB
);
-- 同步时按 ts 分批拉取未推送的 changes,推送后标记
同步策略要点:增量而非全量(只传 changes);幂等(服务端按 row_id+ts 去重);批大小与退避(网络差时减小批次);离线期间本地写照常,恢复后按序回放。
一句话总结:离线优先 = 本地 SQLite 主库 + 变更表 + 增量幂等同步——冲突用版本号 LWW 或 CRDT,网络恢复按序回放。
8. 性能:WASM SQLite 能跑多快
| 操作 | 原生 SQLite(对照) | WASM SQLite(wa-sqlite + OPFS) |
|---|---|---|
| 点查(PK) | ~1µs/行 | ~1-3µs/行(CPU 相当) |
| 批量插入(WAL) | ~50k-200k 行/s | ~30k-150k 行/s |
| 大数据扫描 | 快 | 受 WASM 内存与 OPFS 读吞吐限制 |
| 持久化 | 原生 fsync | OPFS 句柄,relaxed 模式接近原生 |
关键认知:CPU 密集的 SQL 操作,WASM 与原生基本持平(WASM 接近 native 执行速度);真正的瓶颈在 I/O 与内存——OPFS 的读写吞吐、WASM 线性内存上限(默认 2GiB/4GiB)、以及数据从 JS 侧进 SQLite 的拷贝开销。
提速清单:
□ 用 wa-sqlite(OPFS VFS)而非 sql.js(全量导出)
□ 开 WAL 模式:写并发与崩溃恢复都更好
□ 批量事务:BEGIN...COMMIT 包裹大批插入
□ Prepared statements 复用,避免重复 prepare
□ 大数据走 SQL,别把整表拉进 JS 再过滤
一句话总结:WASM SQLite 的 CPU 性能≈原生、瓶颈在 I/O 与内存——选 OPFS VFS、开 WAL、批量事务、复用 prepared statement 是四大提速手段。
9. 选型矩阵与生产建议
| 需求 | 选择 |
|---|---|
| 小数据 / 临时计算 | sql.js(内存 + 手动导出) |
| 正式离线应用(浏览器) | wa-sqlite + OPFS |
| 跨设备/服务端同步 | libsql(嵌入式副本) |
| 边缘本地缓存 | WASI SQLite + Wasmtime |
| 纯 KV(无 SQL 需求) | IndexedDB(不必上 SQLite) |
| 全文检索 | SQLite FTS5(WASM 内可用) |
生产建议:
□ 版本迁移:内置 schema_version + PRAGMA user_version 做迁移
□ 备份:定期 export 到 IndexedDB/云端,防 OPFS 被清
□ 数据安全:敏感列用 WebCrypto 加密再入库
□ 内存:大查询限制返回行数,避免 WASM 内存爆掉
一句话总结:选型 = 数据量 × 是否要同步 × 运行端——浏览器正式应用上 wa-sqlite+OPFS,要同步上 libsql,边缘缓存上 WASI SQLite;别忘了版本迁移与备份。
10. 速查表与一句话记忆
| 问题 | 一句话答案 |
|---|---|
| 为什么 SQLite | 完整 SQL + 单文件 + 零服务进程 |
| 三库 | sql.js 内存兜底 / wa-sqlite OPFS 原生 / libsql 带同步 |
| 编译 | emcc sqlite3.c + 裁剪宏 + ALLOW_MEMORY_GROWTH |
| 持久化 | IndexedDB 存快照、OPFS 给原生句柄 |
| OPFS | createSyncAccessHandle() + relaxed/strict 耐久 |
| WASI 服务端 | wasmtime run --dir /data::/data |
| 离线同步 | 本地主库 + 变更表 + 增量幂等回放 |
| 性能 | CPU≈原生,瓶颈在 I/O 与内存 |
| 提速 | OPFS VFS + WAL + 批量事务 + prepared stmt |
| 生产 | schema 迁移 + 备份 + 加密 + 限行数 |
一句话记忆:WASM SQLite = 把官方 SQLite 编进浏览器——sql.js 内存兜底、wa-sqlite 走 OPFS 原生句柄(WAL 增量写)、libsql 带远程同步;编译用 emcc + 裁剪宏,加载走 initSqlJs;持久化要么 IndexedDB 存快照要么 OPFS 给句柄,OPFS 同步句柄 + relaxed 耐久最贴近原生;同一份代码编 WASI 用 wasmtime run –dir 跑服务端;离线应用 = 本地主库 + 变更表 + 增量幂等同步;性能 CPU≈原生、瓶颈在 I/O——「OPFS VFS + WAL + 批量事务」是铁三角。
延伸阅读
- /wasm-wasi-filesystem-sandbox/ — WASI 文件系统沙箱
- /wasm-embedding-hosting-apis/ — 嵌入与托管 API
- /wasm-performance-optimization/ — 性能优化通论
- /wasm-wasmtime-runtime/ — Wasmtime 运行时详解
- /wasm-multithreading-sharedarraybuffer/ — 多线程与共享内存
- [[nodejs]] — Node.js 的 SQLite 生态对照
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。