</> HTML5Advent
ENFRESDEITPT

// apis · Web Platform Advent #23

IndexedDB, qué es: la base de datos del navegador explicada

IndexedDB es la base de datos de verdad del navegador: asíncrona, transaccional y mucho mayor que localStorage. Cómo funciona onupgradeneeded, por qué se te cierran las transacciones, para qué sirven los índices y cuándo usarla en lugar de Web Storage.

Una pared de cajones de fichero de madera con portaetiquetas de latón y números escritos a mano

IndexedDB es la base de datos de verdad del navegador. Donde el Web Storage te da una caja síncrona de cadenas limitada a unos 5 MB, IndexedDB ofrece un almacén asíncrono y transaccional para datos estructurados, dimensionado en cientos de megabytes o más. No es SQL. Es un almacén clave-valor con índices, y en cuanto eso encaja el resto de la API se sigue solo.

Abrir una base y el único sitio donde puedes cambiar su forma

Se abre una base por su nombre y un número de versión. Si la versión que pides es mayor que la que hay en disco, el navegador dispara upgradeneeded, y ese manejador es el único lugar donde puedes crear o borrar almacenes de objetos e índices.

const request = indexedDB.open('notes-db', 1);

request.onupgradeneeded = (event) => {
  const db = event.target.result;
  const store = db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true });
  store.createIndex('by_tag', 'tag', { unique: false });
};

request.onsuccess = (event) => {
  const db = event.target.result;
};

request.onerror = () => console.error(request.error);

Esto pilla desprevenido porque invierte la costumbre. No puedes crear un almacén de forma perezosa la primera vez que lo necesitas. El esquema vive en una sola función, atada a un número de versión que subes cuando la estructura cambia.

Todo es una petición, y las peticiones son asíncronas

Casi cada llamada de IndexedDB devuelve un IDBRequest en vez de un valor. El resultado se lee en onsuccess y el fallo se trata en onerror. Nada bloquea el hilo principal, que es toda la razón para preferirla a localStorage en cuanto el volumen importa.

Un cajón abierto de fichas con separadores numerados del 166 al 169. Un índice de IndexedDB es exactamente eso: una segunda ordenación de los mismos registros, mantenida para poder buscarlos por algo que no sea su clave.
Un cajón abierto de fichas con separadores numerados del 166 al 169. Un índice de IndexedDB es exactamente eso: una segunda ordenación de los mismos registros, mantenida para poder buscarlos por algo que no sea su clave.

Lecturas y escrituras ocurren dentro de una transacción

No se toca un almacén directamente. Se abre una transacción sobre uno o varios almacenes, en modo readonly o readwrite, y se trabaja a través de ella.

const tx = db.transaction('notes', 'readwrite');
const store = tx.objectStore('notes');

store.add({ title: 'Buy milk', tag: 'errand', done: false });

tx.oncomplete = () => console.log('written');
tx.onerror = () => console.error(tx.error);

Aquí está la trampa que conviene conocer de antemano. Una transacción confirma automáticamente en cuanto no le queda trabajo pendiente. Si esperas algo ajeno en medio, por ejemplo un fetch, la transacción se cierra mientras esperas y la siguiente llamada lanza TransactionInactiveError. Haz primero el trabajo de fuera y abre después la transacción.

Los índices, que son el motivo del nombre

Por defecto los registros se recuperan por su clave primaria. Un índice es una segunda ordenación de los mismos registros que permite consultar por otra propiedad.

const tx = db.transaction('notes', 'readonly');
const index = tx.objectStore('notes').index('by_tag');

const req = index.getAll('errand');
req.onsuccess = () => console.log(req.result);

Para rangos en vez de coincidencias exactas, combina un índice con IDBKeyRange y un cursor, que recorre los resultados de uno en uno en lugar de cargarlos todos en memoria.

Qué puede guardar y cuánto

IndexedDB acepta todo lo que maneja el algoritmo de clonado estructurado: objetos, arrays, Date, Blob, File, ArrayBuffer. Las funciones y los nodos del DOM no son clonables y lanzan error. No hace falta pasar por JSON.stringify, lo que además significa que los números siguen siendo números y las fechas, fechas.

La cuota es una parte del disco disponible y no un tope fijo de 5 MB, así que es el sitio adecuado para respuestas de API en caché, documentos sin conexión o archivos multimedia. Ese almacenamiento es de mejor esfuerzo por defecto y puede desalojarse bajo presión de disco; llama a navigator.storage.persist() si los datos deben sobrevivir a eso, y a navigator.storage.estimate() para ver cuánto consumes.

¿IndexedDB o localStorage?

localStorageIndexedDB
Estilo de APISíncrona, bloquea el hilo principalAsíncrona, basada en eventos
ValoresSolo cadenasValores clonables estructurados
Tamaño orientativoUnos 5 MB por origenUna parte del disco libre
ConsultasSolo por clavePor clave, por índice, por rango con cursores
Bueno paraTema, banderas, preferencias pequeñasDatos sin conexión, cachés, archivos, listas grandes

Ambos están limitados al origen y ninguno es sitio para secretos: cualquier script de la página puede leerlos.

¿Conviene usar una biblioteca envoltorio?

La API en crudo es verbosa porque es anterior a las promesas. Pequeñas bibliotecas envuelven las peticiones en promesas y eliminan casi todo el código repetitivo; son una opción por defecto razonable para código de aplicación. Aun así, aprende antes el modelo de debajo: upgradeneeded, las transacciones y su confirmación automática se comportan igual por dentro, y todos los fallos desconcertantes que te encontrarás vienen de esos tres puntos, no del envoltorio.

Elige IndexedDB cuando los datos sean estructurados, grandes o deban estar disponibles sin conexión, a menudo junto a un service worker. Deja localStorage para el puñado de banderas pequeñas donde una lectura síncrona sea de verdad más simple.