Skip to content

WebAssembly 与 OPFS 离线存储架构

WebAssembly 与 OPFS 离线存储架构

传统的 Web 应用程序重度依赖中心化 API 服务:一旦用户处于飞行模式、地下车库或遭遇弱网抖动,页面便会频繁转圈报错。本地优先 (Local-First) 架构颠覆了这一范式——将客户端本地存储作为数据操作的第一权威源,网络仅作为异步增量同步的背景通道

长期以来,浏览器端的 IndexedDB 存在 API 繁琐、事务锁竞争激烈且缺少复杂 SQL 关联查询能力的缺点。借助 WebAssembly (Wasm) 编译的官方 SQLite,搭配现代浏览器提供的 OPFS (Origin Private File System,源私有文件系统),前端可以直接在浏览器沙箱内获得接近原生操作系统的毫秒级磁盘读写性能。


一、浏览器存储方案横向性能对比

存储技术 接口协议与范式 事务性能 (10,000 条批量写入) 复杂 SQL 查询支持 适用场景
LocalStorage 同步 Key-Value 字符串 (占用主线程) ~1,200 ms (阻塞 UI 严重) ❌ 不支持 仅限少量轻量配置
IndexedDB 异步 NoSQL 对象仓库 ~350 ms (事务上下文切换频繁) ❌ 仅支持基础索引匹配 中小规模对象缓存
Wasm SQLite + OPFS 标准 ANSI SQL,私有文件句柄直读 ~28 ms (🚀 提升 12 倍) ✅ 完整支持 JOIN/Window/全文检索 大型协同文档/离线 ERP/复杂仪表盘
graph TD
    subgraph Browser_Tab [浏览器主线程 (Main UI Thread)]
        UI[React / SolidJS UI 组件交互] --> WorkerBridge[Worker RPC 异步分发]
    end

    subgraph Dedicated_Worker [专用 Web Worker 线程]
        WorkerBridge --> WasmSQLite[SQLite 官方 WebAssembly 引擎]
        WasmSQLite --> OPFS_VFS["OPFS 同步虚拟文件系统 (VFS)"]
    end

    subgraph Sandbox_Disk [浏览器专属持久化磁盘沙箱]
        OPFS_VFS --> FastHandle["FileSystemSyncAccessHandle: 独占无锁二进制读写"]
    end

二、Web Worker 中挂载 OPFS SQLite 实战

因为 OPFS 的 FileSystemSyncAccessHandle 同步高速读写句柄出于安全与防卡死设计仅能在 Web Worker 中运行,我们需要在 Worker 内初始化数据库:

// worker/sqlite-storage.worker.ts
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';

interface QueryMessage {
  type: 'EXECUTE' | 'QUERY';
  sql: string;
  params?: any[];
}

let db: any = null;

async function initSqlite() {
  const sqlite3 = await sqlite3InitModule({
    print: console.log,
    printErr: console.error,
  });

  if ('opfs' in sqlite3) {
    // 实例化 OPFS 高性能持久化数据库
    db = new sqlite3.oo1.OpfsDb('/offline_app.sqlite3');
    console.log('✅ SQLite OPFS Database successfully mounted at /offline_app.sqlite3');

    // 初始化核心业务表结构
    db.exec(`
      CREATE TABLE IF NOT EXISTS notes (
        id TEXT PRIMARY KEY,
        title TEXT NOT NULL,
        content TEXT,
        sync_status INTEGER DEFAULT 0, -- 0: 待同步, 1: 已同步
        updated_at INTEGER NOT NULL
      );
      CREATE INDEX IF NOT EXISTS idx_notes_sync ON notes(sync_status);
    `);
  }
}

self.onmessage = async (event: MessageEvent<QueryMessage>) => {
  if (!db) await initSqlite();

  const { type, sql, params } = event.data;

  try {
    if (type === 'QUERY') {
      const results: any[] = [];
      db.exec({
        sql,
        bind: params,
        rowMode: 'object',
        callback: (row: any) => results.push(row),
      });
      self.postMessage({ success: true, data: results });
    } else {
      db.exec({ sql, bind: params });
      self.postMessage({ success: true, rowsModified: db.changes() });
    }
  } catch (err: any) {
    self.postMessage({ success: false, error: err.message });
  }
};

三、双向增量同步与冲突解决机制

在 Local-First 应用中,客户端离线状态下编辑的数据记录为 sync_status = 0。当检测到 navigator.onLine 恢复时,启动增量同步流:

// services/sync-engine.ts
export class OfflineSyncEngine {
  private isSyncing = false;

  async triggerSync() {
    if (!navigator.onLine || this.isSyncing) return;
    this.isSyncing = true;

    try {
      // 1. 查询本地所有未同步的变更记录
      const dirtyRecords = await this.queryLocal(
        'SELECT * FROM notes WHERE sync_status = 0 ORDER BY updated_at ASC'
      );

      if (dirtyRecords.length > 0) {
        // 2. 批量推送到云端
        const response = await fetch('/api/sync/batch-push', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ changes: dirtyRecords }),
        });

        if (response.ok) {
          const { appliedIds } = await response.json();
          // 3. 标记本地状态为已同步
          await this.executeLocal(
            `UPDATE notes SET sync_status = 1 WHERE id IN (${appliedIds.map(() => '?').join(',')})`,
            appliedIds
          );
        }
      }
    } finally {
      this.isSyncing = false;
    }
  }

  private queryLocal(sql: string, params: any[] = []): Promise<any[]> {
    // 封装向 SQLite Web Worker 发起的请求
    return new Promise((resolve) => { /* ... */ });
  }
  private executeLocal(sql: string, params: any[] = []): Promise<void> {
    return new Promise((resolve) => { /* ... */ });
  }
}

四、工程落地最佳实践与注意事项

  1. 配置 COOP / COEP 跨域隔离安全头:为了解锁 Wasm 使用 SharedArrayBuffer 实现多线程,服务端必须下发以下 HTTP 响应头:
    Cross-Origin-Opener-Policy: same-origin
    Cross-Origin-Embedder-Policy: require-corp
    
  2. 磁盘配额防耗尽预警:调用 navigator.storage.estimate() 监控已使用字节数与总配额,当可用空间低于 10% 时及时提醒用户清理旧数据。