12k
All articles

Executando SQLite no Navegador Com OPFS

Use SQLite no navegador com OPFS: configure um Worker, escolha opfs ou opfs-sahpool e evite fallback silencioso para memória.

OpenReplay Team
OpenReplay Team
Executando SQLite no Navegador Com OPFS

O SQLite compilado para WebAssembly roda inteiramente em memória por padrão, portanto cada linha que você grava desaparece ao atualizar a página, a menos que o banco de dados seja respaldado por um VFS persistente — e o Origin Private File System (OPFS) é justamente o que fornece essa persistência no build oficial.

Chegar lá exige um pouco mais do que um import e uma query. Este artigo percorre o custo real de configuração com o pacote oficial @sqlite.org/sqlite-wasm: abrir um banco de dados persistente em um Worker, conectá-lo à UI, as duas restrições que derrubam as primeiras implementações e como escolher entre os dois VFSes viáveis em produção.

Pontos Principais

  • O build oficial é publicado no npm como @sqlite.org/sqlite-wasm e, diferente do sql.js, é mantido pelo próprio projeto SQLite, com persistência via OPFS já integrada.
  • Os synchronous access handles do OPFS existem apenas em threads de Worker, portanto um banco de dados respaldado por OPFS nunca pode ser aberto na thread principal.
  • O VFS padrão “opfs” requer os headers COOP e COEP porque depende de SharedArrayBuffer; o VFS “opfs-sahpool” não exige header nenhum.
  • O opfs-sahpool é a opção OPFS mais rápida para trabalho em lote, mas permite apenas uma conexão aberta, de modo que uma segunda aba abrindo o mesmo banco de dados falha.
  • Nunca faça fallback silencioso para um banco de dados em memória quando o OPFS estiver indisponível; isso mantém a aplicação funcionando enquanto descarta tudo o que o usuário salva.

O Que o OPFS Oferece ao SQLite no Navegador?

O OPFS é um sistema de arquivos isolado em sandbox e com escopo de origem que dá ao SQLite exatamente o que ele precisa: acesso síncrono a arquivos em nível de byte que sobrevive a recarregamentos. Sem ele, o build Wasm mantém o banco de dados em memória e um refresh apaga tudo. Com ele, você obtém um banco de dados SQL realmente durável no cliente, que é a peça que torna os recursos modernos do SQLite utilizáveis em um contexto de navegador.

Use o pacote oficial. Existem ports Wasm comunitários mais antigos, mas o @sqlite.org/sqlite-wasm é o próprio build Wasm do projeto SQLite, republicado como um módulo ES. A única coisa adicionada por cima é um conjunto de tipos TypeScript. A documentação de persistência descreve vários backends de armazenamento; os dois que importam na prática são o VFS “opfs” e o VFS “opfs-sahpool”. Existem outros caminhos (kvvfs sobre localStorage, uma variante “opfs-wl”, modo WAL com locking exclusivo), todos cobertos nesse mesmo documento.

Abrindo um Banco de Dados Dentro de um Worker

Instale o pacote e depois faça todo o trabalho de banco de dados em um Worker dedicado. Este exemplo usa o VFS “opfs-sahpool”, que deve ser instalado explicitamente com await sqlite3.installOpfsSAHPoolVfs(). Ele normaliza nomes de bancos de dados para caminhos absolutos, então use a barra inicial de forma consistente:

npm install @sqlite.org/sqlite-wasm
// worker.js
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';

let db;

async function init() {
  const sqlite3 = await sqlite3InitModule();
  const poolUtil = await sqlite3.installOpfsSAHPoolVfs();
  db = new poolUtil.OpfsSAHPoolDb('/app.sqlite3'); // names are normalised to an absolute path
  db.exec('CREATE TABLE IF NOT EXISTS notes(id INTEGER PRIMARY KEY, body TEXT)');
}

init()
  .then(() => postMessage({ type: 'ready' }))
  .catch((err) => postMessage({ type: 'init-error', message: err.message }));

Observe o que este código não faz: recorrer a new sqlite3.oo1.DB(...) quando o OPFS estiver indisponível. Esse padrão aparece em muito código de exemplo, incluindo o exemplo de worker do README oficial, e é um bug de perda de dados disfarçado. A aplicação continua funcionando contra um banco de dados transitório em memória, o usuário continua salvando, e um refresh destrói tudo. Se a persistência falhar ao inicializar, exponha o erro na UI e informe o usuário.

Conversando com o Worker a Partir da UI

A biblioteca ainda exporta uma API promiser para acesso pela thread principal, mas o README do pacote marca as APIs Worker1 e Promiser1 como deprecated em um aviso datado de 2026-04-15. Elas permanecem no pacote, não recebem mais trabalho, e os mantenedores desencorajam seu uso. O caminho documentado é sqlite3InitModule mais a API oo1 dentro do Worker, com uma ponte fina de postMessage feita por você:

// worker.js (continued)
onmessage = ({ data }) => {
  const { id, sql, bind } = data;
  try {
    const rows = db.exec({ sql, bind, rowMode: 'object', returnValue: 'resultRows' });
    postMessage({ id, result: rows });
  } catch (err) {
    postMessage({ id, error: err.message });
  }
};
// db-client.js (main thread)
const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });
let nextId = 1;
const pending = new Map();

worker.onmessage = ({ data }) => {
  const entry = pending.get(data.id);
  if (!entry) return;
  pending.delete(data.id);
  data.error ? entry.reject(new Error(data.error)) : entry.resolve(data.result);
};

export function query(sql, bind = []) {
  return new Promise((resolve, reject) => {
    const id = nextId++;
    pending.set(id, { resolve, reject });
    worker.postMessage({ id, sql, bind });
  });
}

Quarenta linhas de ponte é todo o custo, e você controla o formato das mensagens.

Armadilha Um: SQLite com OPFS Deve Rodar em um Worker

SQLite respaldado por OPFS não pode rodar na thread principal, ponto final. O SQLite é um motor síncrono, e o acesso síncrono a arquivos de que ele precisa vem de FileSystemSyncAccessHandle, que a plataforma expõe apenas dentro de Web Workers dedicados, precisamente porque I/O síncrono bloqueia qualquer thread que o execute.

Quando essa restrição é contornada em vez de respeitada, o session replay torna a falha inequívoca: o replay mostra cliques e digitações sendo registrados enquanto nada é repintado durante o tempo de uma query — a assinatura visual do I/O síncrono do OPFS bloqueando a thread da UI. Mantenha o motor no Worker e a thread principal nunca verá uma query.

Armadilha Dois: A Exigência dos Headers COOP/COEP

O VFS padrão “opfs” usa SharedArrayBuffer para passar mensagens entre seu front end síncrono e o worker assíncrono que fica por trás dele, portanto o servidor deve enviar Cross-Origin-Opener-Policy: same-origin e Cross-Origin-Embedder-Policy: require-corp ou o VFS não carregará. Para o Vite, o README oficial fornece esta configuração, incluindo a exclusão necessária em optimizeDeps:

import { defineConfig } from 'vite';

export default defineConfig({
  server: {
    headers: {
      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Embedder-Policy': 'require-corp',
    },
  },
  optimizeDeps: {
    exclude: ['@sqlite.org/sqlite-wasm'],
  },
});

Servidores de produção precisam dos mesmos dois headers. Mas se você não puder definir headers (hospedagem estática, embeds de terceiros que o COEP quebra), não é preciso recorrer a gambiarras com service worker: o VFS “opfs-sahpool” não requer headers COOP/COEP algum, e é por isso que o código acima o utiliza.

Escolhendo Entre os Dois VFSes OPFS

Escolha “opfs” quando múltiplas abas precisarem compartilhar um mesmo banco de dados e você controlar os headers; escolha “opfs-sahpool” quando quiser velocidade máxima sem exigência de headers e puder conviver com uma única conexão. A própria documentação de persistência do SQLite classifica o sahpool como o mais rápido dos backends OPFS que ela cobre. Você não sentirá a diferença ao salvar um único registro. Você sentirá em trabalho em massa.

”opfs""opfs-sahpool”
Headers COOP/COEPObrigatóriosNão obrigatórios
Múltiplas conexões/abasSim, com tratamento de SQLITE_BUSYNão, uma por vez
DesempenhoBomO mais rápido para trabalho em lote, segundo a documentação do SQLite
RegistroAutomático quando suportadoExplícito via installOpfsSAHPoolVfs()
Safari 16.4 a 16.xQuebrado por um bug de sub-worker no WebKitFunciona

Multi-aba não é gratuito nem mesmo no “opfs”. Adquirir um sync access handle bloqueia o arquivo exclusivamente, e a leitura também toma esse lock, portanto uma segunda aba abrindo o mesmo banco de dados encontra um erro de locking que se manifesta como SQLITE_BUSY ou um erro genérico de I/O. Trate-o em vez de considerá-lo fatal:

async function withRetry(fn, attempts = 5, delayMs = 100) {
  for (let i = 0; i < attempts; i++) {
    try {
      return fn();
    } catch (err) {
      if (!/SQLITE_BUSY/.test(String(err.message)) || i === attempts - 1) throw err;
      await new Promise((r) => setTimeout(r, delayMs));
    }
  }
}

Mantenha as transações curtas e os statements resetados, e a concorrência moderada entre abas funciona. No sahpool, a chamada de installOpfsSAHPoolVfs() de uma segunda aba falha de imediato: o pool toma o lock do banco de dados para si, portanto uma conexão é o teto. Detecte isso e roteie a segunda aba através da primeira. O SQLite 3.50 adicionou pauseVfs() e unpauseVfs() para esse tipo de repasse cooperativo.

Quando Você Deve Escolher SQLite em Vez de IndexedDB?

Opte por SQLite sobre OPFS quando seus dados forem relacionais: joins entre entidades, agregações, filtragem ad-hoc, índices SQL completos, ou entrega de um dataset pré-construído como um único arquivo de banco de dados que você importa uma vez. Essas são as cargas de trabalho em que o IndexedDB obriga você a reimplementar um motor de consultas no código da aplicação.

É exagero para estado key-value, caches pequenos ou algumas centenas de registros. Esse trabalho não justifica um binário Wasm, um Worker e uma ponte de mensagens; localStorage ou IndexedDB puro têm o tamanho certo.

Concluindo

O custo de configuração é real, mas limitado: um Worker, uma ponte de mensagens e uma decisão de VFS que depende de você precisar ou não de acesso multi-aba ou de deploy sem headers. Comece com “opfs-sahpool” para uma aplicação local-first de documento único, migre para “opfs” mais tratamento de SQLITE_BUSY quando as abas precisarem compartilhar, e nunca deixe uma falha de inicialização do OPFS degradar silenciosamente para um banco de dados em memória.

Perguntas Frequentes

Quais navegadores suportam SQLite Wasm com persistência OPFS?

Os synchronous access handles do OPFS estão disponíveis a partir do Chromium 108, Firefox 111 e Safari 16.4. Uma ressalva: versões do Safari abaixo da 17 carregam um bug de sub-worker no WebKit que quebra o VFS 'opfs' padrão, e a documentação do SQLite aponta o 'opfs-sahpool' como a opção que ainda funciona nesses casos. Safari 17 e posteriores executam ambos os VFSes.

Posso distribuir um arquivo de banco de dados SQLite pré-construído e carregá-lo no OPFS?

Sim. Com o VFS 'opfs-sahpool', faça fetch do arquivo .db como um ArrayBuffer e passe-o para importDb() no objeto PoolUtil resolvido por installOpfsSAHPoolVfs(), e depois abra o banco de dados normalmente. Passe exatamente a mesma string de nome para ambas as chamadas: importDb() armazena o nome exatamente como você o fornece, enquanto a abertura de um banco de dados o normaliza para um caminho absoluto, portanto importar 'data.db' e depois abrir '/data.db' deixa você com um banco de dados vazio. O PoolUtil também oferece exportFile() para extrair um banco de dados para backup e getFileNames() para listar o que o pool contém. Isso serve bem a aplicações que distribuem datasets de referência como um único arquivo.

Quantos dados um banco de dados SQLite respaldado por OPFS pode armazenar?

Não há limite fixo. O armazenamento OPFS está sujeito a quotas gerenciadas pelo navegador, que são generosas mas variam conforme o navegador, o dispositivo e o espaço em disco disponível, portanto verifique navigator.storage.estimate() em tempo de execução em vez de assumir um número. Janelas privadas e de navegação anônima podem reduzir ou eliminar completamente a persistência, e limpar os dados do site apaga o banco de dados junto com o restante do armazenamento da origem.

Como inspeciono os arquivos OPFS que o SQLite cria durante a depuração?

As DevTools dos navegadores não exibem o conteúdo do OPFS nativamente. A extensão OPFS Explorer para o Chrome DevTools mostra a hierarquia de arquivos OPFS da origem e permite baixar arquivos individuais. Note que o 'opfs-sahpool' armazena bancos de dados dentro de arquivos de pool opacos sob seu próprio mapeamento de nomes virtuais, portanto o nome de arquivo que você passou não aparecerá diretamente; use getFileNames() e exportFile() do PoolUtil para listar e extrair esses bancos de dados.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.