Ejecutar SQLite en el navegador con OPFS
Usa SQLite en el navegador con OPFS: configura un Worker, elige opfs o opfs-sahpool y evita volver sin aviso a memoria.
SQLite compilado a WebAssembly se ejecuta enteramente en memoria de forma predeterminada, por lo que cada fila que escribes desaparece al recargar la página, a menos que la base de datos esté respaldada por un VFS persistente, y el Origin Private File System (OPFS) es lo que proporciona esa persistencia en la compilación oficial.
Llegar hasta ahí requiere algo más que un import y una consulta. Este artículo recorre el costo real de configuración con el paquete oficial @sqlite.org/sqlite-wasm: abrir una base de datos persistente en un Worker, conectarla con la UI, las dos restricciones que hacen tropezar a las primeras implementaciones y cómo elegir entre los dos VFS viables en producción.
Puntos clave
- La compilación oficial se publica en npm como @sqlite.org/sqlite-wasm y, a diferencia de sql.js, la mantiene el propio proyecto SQLite, con persistencia OPFS incorporada.
- Los handles de acceso síncrono de OPFS solo existen en hilos Worker, por lo que una base de datos respaldada por OPFS nunca puede abrirse en el hilo principal.
- El VFS “opfs” predeterminado requiere los encabezados COOP y COEP porque depende de SharedArrayBuffer; el VFS “opfs-sahpool” no necesita ningún encabezado.
- opfs-sahpool es la opción OPFS más rápida para trabajo por lotes, pero solo admite una conexión abierta, de modo que una segunda pestaña que abra la misma base de datos fallará.
- Nunca recurras silenciosamente a una base de datos en memoria cuando OPFS no esté disponible; eso mantiene la aplicación funcionando mientras descarta todo lo que el usuario guarda.
¿Qué le aporta OPFS a SQLite en el navegador?
OPFS es un sistema de archivos aislado en un sandbox y con alcance de origen que le da a SQLite lo que realmente necesita: acceso síncrono a archivos a nivel de bytes que sobrevive a las recargas. Sin él, la compilación Wasm mantiene la base de datos en memoria y una recarga la borra. Con él, obtienes una base de datos SQL realmente duradera en el cliente, que es la pieza que hace que las funcionalidades modernas de SQLite sean utilizables en un contexto de navegador.
Usa el paquete oficial. Existen ports Wasm comunitarios más antiguos, pero @sqlite.org/sqlite-wasm es la compilación Wasm propia del proyecto SQLite, republicada como módulo ES. Lo único que se añade encima es un conjunto de tipos de TypeScript. La documentación sobre persistencia describe varios backends de almacenamiento; los dos que importan en la práctica son el VFS “opfs” y el VFS “opfs-sahpool”. Existen otras vías (kvvfs sobre localStorage, una variante “opfs-wl”, modo WAL con bloqueo exclusivo), todas cubiertas en ese mismo documento.
Abrir una base de datos dentro de un Worker
Instala el paquete y luego realiza todo el trabajo de base de datos en un Worker dedicado. Este ejemplo usa el VFS “opfs-sahpool”, que debe instalarse explícitamente con await sqlite3.installOpfsSAHPoolVfs(). Este normaliza los nombres de base de datos a rutas absolutas, así que usa la 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 }));
Fíjate en lo que este código no hace: recurrir a new sqlite3.oo1.DB(...) cuando OPFS no está disponible. Ese patrón aparece en mucho código de ejemplo, incluido el ejemplo de worker del README oficial, y es un bug de pérdida de datos disfrazado. La aplicación sigue funcionando contra una base de datos transitoria en memoria, el usuario sigue guardando y una recarga lo destruye todo. Si la persistencia no logra inicializarse, expón el error en la UI y avisa al usuario.
Comunicarse con el Worker desde la UI
La biblioteca todavía exporta una API promiser para acceso desde el hilo principal, pero el README del paquete marca las APIs Worker1 y Promiser1 como obsoletas en un aviso fechado el 2026-04-15. Permanecen en el paquete, no reciben más trabajo y los mantenedores desaconsejan su uso. La vía documentada es sqlite3InitModule más la API oo1 dentro del Worker, con un puente ligero de postMessage propio:
// 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 });
});
}
Cuarenta líneas de puente es todo el costo, y tú controlas la forma de los mensajes.
Tropiezo uno: SQLite con OPFS debe ejecutarse en un Worker
SQLite respaldado por OPFS no puede ejecutarse en el hilo principal, punto. SQLite es un motor síncrono, y el acceso síncrono a archivos que necesita proviene de FileSystemSyncAccessHandle, que la plataforma expone únicamente dentro de Web Workers dedicados, precisamente porque la E/S síncrona bloquea el hilo que la ejecuta.
Cuando se rodea esta restricción en lugar de respetarla, la reproducción de sesiones hace que el fallo sea inconfundible: la reproducción muestra clics y pulsaciones de teclas registrándose mientras nada se repinta durante lo que dura una consulta, la firma visual de la E/S síncrona de OPFS bloqueando el hilo de la UI. Mantén el motor en el Worker y el hilo principal nunca verá una consulta.
Tropiezo dos: el requisito de encabezados COOP/COEP
El VFS “opfs” predeterminado usa SharedArrayBuffer para pasar mensajes entre su front end síncrono y el worker asíncrono que está detrás, por lo que el servidor debe enviar Cross-Origin-Opener-Policy: same-origin y Cross-Origin-Embedder-Policy: require-corp o el VFS no se cargará. Para Vite, el README oficial ofrece esta configuración, incluida la exclusión requerida en 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'],
},
});
Los servidores de producción necesitan esos mismos dos encabezados. Pero si no puedes configurar encabezados (hosting estático, integraciones de terceros que COEP rompe), no necesitas un truco con service worker: el VFS “opfs-sahpool” no requiere ningún encabezado COOP/COEP, y por eso el código anterior lo utiliza.
Elegir entre los dos VFS de OPFS
Elige “opfs” cuando varias pestañas deban compartir una misma base de datos y controles los encabezados; elige “opfs-sahpool” cuando quieras máxima velocidad sin requisitos de encabezados y puedas vivir con una sola conexión. La propia documentación de persistencia de SQLite califica a sahpool como el más rápido de los backends OPFS que cubre. No notarás la diferencia al guardar un solo registro. Sí la notarás en trabajo masivo.
| ”opfs" | "opfs-sahpool” | |
|---|---|---|
| Encabezados COOP/COEP | Requeridos | No requeridos |
| Múltiples conexiones/pestañas | Sí, con manejo de SQLITE_BUSY | No, una a la vez |
| Rendimiento | Bueno | El más rápido para trabajo por lotes, según la documentación de SQLite |
| Registro | Automático cuando es compatible | Explícito con installOpfsSAHPoolVfs() |
| Safari 16.4 a 16.x | Roto por un bug de sub-workers en WebKit | Funciona |
El soporte multipestaña no es gratuito ni siquiera con “opfs”. Adquirir un handle de acceso síncrono bloquea el archivo de forma exclusiva, y leer también toma ese bloqueo, así que una segunda pestaña que abra la misma base de datos se topa con un error de bloqueo que aparece como SQLITE_BUSY o como un error genérico de E/S. Manéjalo en lugar de tratarlo como 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));
}
}
}
Mantén las transacciones cortas y las sentencias reiniciadas, y una concurrencia moderada entre pestañas funciona. En sahpool, la llamada a installOpfsSAHPoolVfs() de una segunda pestaña falla directamente: el pool toma el bloqueo de la base de datos para sí mismo, por lo que una conexión es el límite. Detéctalo y enruta la segunda pestaña a través de la primera. SQLite 3.50 añadió pauseVfs() y unpauseVfs() para ese tipo de traspaso cooperativo.
¿Cuándo deberías elegir SQLite en lugar de IndexedDB?
Recurre a SQLite sobre OPFS cuando tus datos sean relacionales: joins entre entidades, agregaciones, filtrado ad-hoc, índices SQL completos o distribuir un conjunto de datos precompilado como un único archivo de base de datos que importas una sola vez. Esas son las cargas de trabajo en las que IndexedDB te obliga a reimplementar un motor de consultas en el código de la aplicación.
Es excesivo para estado clave-valor, cachés pequeñas o unos pocos cientos de registros. Ese trabajo no justifica un binario Wasm, un Worker y un puente de mensajes; localStorage o IndexedDB a secas son del tamaño adecuado.
Conclusión
El costo de configuración es real pero acotado: un Worker, un puente de mensajes y una decisión de VFS que depende de si necesitas acceso multipestaña o despliegue sin encabezados. Empieza con “opfs-sahpool” para una aplicación local-first de un solo documento, pasa a “opfs” más el manejo de SQLITE_BUSY cuando las pestañas deban compartir, y nunca dejes que un fallo de inicialización de OPFS se degrade silenciosamente hacia una base de datos en memoria.
Preguntas frecuentes
¿Qué navegadores admiten SQLite Wasm con persistencia OPFS?
Los handles de acceso síncrono de OPFS están disponibles desde Chromium 108, Firefox 111 y Safari 16.4 en adelante. Una advertencia: las versiones de Safari anteriores a la 17 arrastran un bug de sub-workers en WebKit que rompe el VFS 'opfs' predeterminado, y la documentación de SQLite señala 'opfs-sahpool' como la opción que sí funciona ahí. Safari 17 y posteriores ejecutan ambos VFS.
¿Puedo distribuir un archivo de base de datos SQLite precompilado y cargarlo en OPFS?
Sí. Con el VFS 'opfs-sahpool', descarga el archivo .db como ArrayBuffer y pásalo a importDb() en el objeto PoolUtil que resuelve installOpfsSAHPoolVfs(), y luego abre la base de datos normalmente. Pasa exactamente la misma cadena de nombre a ambas llamadas: importDb() almacena el nombre tal cual lo indicas, mientras que abrir una base de datos lo normaliza a una ruta absoluta, así que importar 'data.db' y luego abrir '/data.db' te dejará con una base de datos vacía. PoolUtil también ofrece exportFile() para extraer una base de datos como copia de seguridad y getFileNames() para listar lo que contiene el pool. Esto encaja con aplicaciones que distribuyen conjuntos de datos de referencia en un único archivo.
¿Cuántos datos puede almacenar una base de datos SQLite respaldada por OPFS?
No hay un límite fijo. El almacenamiento OPFS está sujeto a cuotas gestionadas por el navegador que son generosas pero varían según el navegador, el dispositivo y el espacio en disco disponible, así que consulta navigator.storage.estimate() en tiempo de ejecución en lugar de asumir una cifra. Las ventanas privadas o de incógnito pueden reducir o eliminar por completo la persistencia, y borrar los datos del sitio elimina la base de datos junto con el resto del almacenamiento del origen.
¿Cómo inspecciono los archivos OPFS que crea SQLite durante la depuración?
Las DevTools de los navegadores no muestran el contenido de OPFS de forma nativa. La extensión OPFS Explorer para Chrome DevTools muestra la jerarquía de archivos OPFS del origen y permite descargar archivos individuales. Ten en cuenta que 'opfs-sahpool' almacena las bases de datos dentro de archivos de pool opacos bajo su propio mapeo de nombres virtuales, por lo que el nombre de archivo que pasaste no aparecerá directamente; usa getFileNames() y exportFile() de PoolUtil para listar y extraer esas bases de datos.
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