Los dos ejes que ordenan el mapa: síncrono y estructurado
Síncrono frente a asíncrono, y estructurado frente a texto plano: dos preguntas que colocan los seis mecanismos en un plano y resuelven casi cualquier decisión de almacenamiento.
Seis mecanismos son demasiados para recordarlos como una lista, pero son muy pocos cuando se los coloca sobre dos ejes. El primero pregunta quién paga la espera: si la operación bloquea al hilo que la invoca o le devuelve el control de inmediato. El segundo pregunta qué forma tiene el dato: si el almacén guarda una cadena que tú debes serializar y analizar entera, o valores estructurados a los que se accede por partes. Estas dos preguntas no son una simplificación pedagógica: son las dos decisiones de diseño que efectivamente distinguen a las APIs entre sí, y quien las tiene interiorizadas resuelve la elección de almacenamiento en segundos, sin consultar tablas.
- Formular el eje de la sincronía y entender por qué el hilo que bloquea importa más que el bloqueo.
- Formular el eje de la forma del dato y sus consecuencias sobre acceso parcial e indexación.
- Situar los seis mecanismos del inventario en los cuadrantes que definen ambos ejes.
- Reconocer los costes síncronos que sobreviven dentro de una API asíncrona.
El primer eje: quién paga la espera
Empecemos deshaciendo el eslogan, porque circula mucho y es la causa de decisiones malas en ambas direcciones. La formulación ingenua del eje dice que lo síncrono es malo y lo asíncrono bueno. Es falsa, y la prueba está en el propio inventario: OPFS ofrece acceso síncrono y es el sustrato sobre el que corren las bases de datos más rápidas del navegador. La formulación correcta es otra: lo que importa no es si la operación bloquea, sino a qué hilo bloquea.
Dicho de otro modo: la sincronía no es una propiedad moral de una API, es una relación entre la operación y el hilo que la ejecuta. Cambia el hilo y cambia el veredicto, sin que la operación haya cambiado en absoluto.
Bloquear el hilo principal es inaceptable porque ese hilo también ejecuta el layout, la pintura y los manejadores de entrada; detenerlo congela la aplicación entera. Bloquear un Worker dedicado es perfectamente legítimo, y de hecho es deseable: un motor como SQLite está escrito suponiendo lecturas y escrituras que devuelven el dato, y forzarlo a un modelo asíncrono lo obligaría a mantener continuaciones en cada punto de espera. Por eso los manejadores de acceso síncrono de OPFS solo existen dentro de un Worker: la especificación codifica exactamente esta distinción.
flowchart TD
A[Operacion de almacenamiento] --> B{en que hilo se ejecuta}
B -->|hilo principal| C{la API es sincrona}
B -->|worker dedicado| D[sincrono aceptable y hasta preferible]
C -->|si| E[la interfaz se congela]
C -->|no| F[la interfaz sigue viva]
style E fill:#f38ba8,color:#11111b
style D fill:#a6e3a1,color:#11111b
style F fill:#a6e3a1,color:#11111bLa consecuencia práctica de esta reformulación es que el eje no tiene dos posiciones sino tres, y la tercera es la interesante. Están las APIs síncronas en el hilo principal, que son las que hay que evitar; están las asíncronas, que son el recurso general; y está la sincronía reubicada, que consiste en mover el trabajo a un hilo donde bloquear es gratis y comunicarse con él por mensajes. Esta tercera posición es la que hace posible ejecutar un motor de base de datos completo dentro de una pestaña sin que la interfaz lo note.
// La tercera posicion del eje: sincronia dentro de un Worker dedicado
// worker.js
const raiz = await navigator.storage.getDirectory();
const manejador = await (await raiz.getFileHandle("datos.db", { create: true }))
.createSyncAccessHandle(); // solo existe aqui, nunca en el hilo principal
const buffer = new DataView(new ArrayBuffer(4096));
manejador.read(buffer, { at: 0 }); // devuelve el dato, sin promesa
manejador.write(buffer, { at: 0 }); // bloquea este hilo, y no pasa nada
Hay un matiz que separa a quien entiende el eje de quien solo lo ha memorizado: una API asíncrona puede seguir cobrando costes síncronos en el hilo que la llama. Cuando escribes un objeto grande en IndexedDB, el algoritmo de clonado estructurado se ejecuta antes de que la operación se encole, y ese trabajo lo paga el hilo que llamó. La promesa te devuelve el control después, pero la serialización ya ocurrió donde estabas. Asíncrono significa no espero al resultado, no significa no hago trabajo.
Una transacción de IndexedDB permanece activa mientras se le sigan encolando peticiones dentro del mismo giro del bucle de eventos o de los callbacks encadenados a sus peticiones. Si intercalas un await sobre algo ajeno a la transacción, esta se compromete sola y la siguiente operación falla con un error de estado. Es la trampa más frecuente de esta API y es consecuencia directa de mezclar dos modelos de asincronía: el de eventos con el que se diseñó y el de promesas con el que la usamos hoy.
El segundo eje: qué forma tiene el dato
El segundo eje distingue almacenes que guardan una cadena opaca de almacenes que guardan valores con estructura. La diferencia parece cosmética —al fin y al cabo, un objeto siempre puede serializarse a JSON— y es en realidad la más determinante de las dos.
Formulado con precisión, el eje pregunta quién conoce la forma del dato. En un almacén textual la conoce solo tu código, y el almacén ve una cadena opaca; en un almacén estructurado la conoce también el almacén, y esa es la condición necesaria para que pueda indexar, filtrar y devolverte fragmentos. Toda la diferencia práctica se sigue de ahí.
En un almacén de texto plano, tú eres responsable de la serialización: cada escritura implica un JSON.stringify completo y cada lectura un JSON.parse completo, aunque solo te interese un campo. No hay acceso parcial ni indexación: leer el nombre de un usuario obliga a materializar el objeto entero. Y no hay tipos: fechas, mapas, conjuntos y binarios se degradan a su representación textual, con el binario pagando además el impuesto de base64.
En un almacén estructurado, el navegador aplica el algoritmo de clonado estructurado y conserva la forma del valor: objetos anidados, arrays, Date, Map, Set, ArrayBuffer, Blob. Y aparece lo que el eje realmente compra: acceso parcial. Puedes recuperar una entrada por clave sin tocar las demás, definir índices sobre campos y recorrer rangos con un cursor sin materializar el conjunto.
El clonado estructurado tiene sus propias fronteras, y conocerlas evita sorpresas. No clona funciones ni nada que las contenga; no clona nodos del DOM; y, crucialmente, no conserva el prototipo: una instancia de tu clase entra como instancia y sale como objeto plano con los mismos campos. Si tu modelo de dominio depende de métodos, tendrás que rehidratar al leer, y ese paso de rehidratación es un coste real que hay que contabilizar en el diseño y no descubrir en producción.
La diferencia entre ambos extremos no es de comodidad sino de complejidad asintótica. En un almacén textual, el coste de leer un campo es proporcional al tamaño total del documento, porque hay que analizarlo entero; en un almacén estructurado con índices, es proporcional al tamaño del resultado. Esa distinción, que en un objeto de dos campos es invisible, decide si tu aplicación sigue siendo utilizable cuando el usuario lleve dos años acumulando datos.
Texto plano
Cookies y Web Storage. Serializas tú, lees entero, no hay índices y el binario pasa por base64.
Estructurado
IndexedDB y Cache API. Clonado estructurado, tipos conservados, acceso por clave, índices y cursores.
Bytes con acceso aleatorio
OPFS. No impone forma alguna: ofrece desplazamientos, lecturas y escrituras parciales sobre archivos.
Por qué OPFS es el extremo
Al no imponer estructura, permite que un motor real la imponga encima. SQLite sobre OPFS existe justamente por eso.
// Texto plano: cada lectura materializa el objeto entero
const usuarios = JSON.parse(localStorage.getItem("usuarios")); // todo en memoria
const uno = usuarios.find((u) => u.id === 42);
// Estructurado: acceso por clave, sin tocar el resto del almacen
const tx = db.transaction("usuarios", "readonly");
const uno2 = await tx.objectStore("usuarios").get(42); // solo esa entrada
// Estructurado con indice: el coste depende del resultado, no del total
const activos = await tx.objectStore("usuarios").index("estado").getAll("activo");
Los dos primeros bloques hacen lo mismo desde fuera y son incomparables por dentro: el primero paga el conjunto completo para devolver un elemento; el segundo paga aproximadamente el elemento. El tercero generaliza esa propiedad a las consultas, y es lo que convierte un almacén en una base de datos.
El plano completo
Cruzando ambos ejes aparece el mapa que sustituye a la lista, y su virtud principal es que se dibuja de memoria en una servilleta: dos rectas, cuatro casillas y seis nombres. Los tres mecanismos históricos ocupan el mismo cuadrante —síncrono y textual—, lo que explica que se confundan entre sí y que ninguno sirva para datos de aplicación. IndexedDB y Cache API ocupan el cuadrante asíncrono y estructurado, que es donde vive el grueso del almacenamiento serio en el navegador. Y OPFS aparece dos veces, porque expone una interfaz asíncrona en el hilo principal y una síncrona dentro de un Worker: la misma API en dos cuadrantes, según dónde la invoques.
| Mecanismo | Sincronía | Forma del dato |
|---|---|---|
| Cookies | Síncrono en el hilo principal | Cadena única, clave | valor a mano |
localStorage |
Síncrono en el hilo principal | Cadena a cadena |
sessionStorage |
Síncrono en el hilo principal | Cadena a cadena |
| IndexedDB | Asíncrono, con coste de clonado | Estructurado, con índices y cursores |
| Cache API | Asíncrono | Pares de Request | Response |
| OPFS | Asíncrono fuera, síncrono en Worker | Bytes con acceso aleatorio |
Las tres primeras filas son intercambiables en ambos ejes y solo difieren en ámbito y visibilidad, que fue justamente el tema de la lección anterior: por eso se confunden tanto.
Que OPFS aparezca dos veces no es una anomalía de la tabla: es la propiedad que lo hace especial. Ningún otro mecanismo del inventario cambia de cuadrante según el contexto de ejecución, y esa dualidad es exactamente lo que se necesitaba para portar motores escritos en C al navegador sin reescribir su modelo de entrada y salida. La API se adapta al hilo, en lugar de obligar al hilo a adaptarse a la API.
Ambas son asíncronas y estructuradas, y por eso es tentador tratarlas como intercambiables. No lo son: la Cache API indexa por Request y guarda Response con sus cabeceras, lo que la hace perfecta para servir recursos sin red y torpe para consultar datos, porque no tiene índices secundarios ni cursores sobre campos. Compartir cuadrante significa compartir restricciones de ejecución, no propósito; el segundo eje ordena la forma del dato, no su semántica.
No existe ningún mecanismo síncrono y estructurado accesible desde el hilo principal, y la ausencia no es casual: sería la combinación más cómoda de usar y la más destructiva para la interfaz, porque juntaría el bloqueo del hilo con el coste de deserializar estructuras grandes. Que la plataforma nunca la haya ofrecido es una decisión de diseño deliberada, y leerla como tal enseña más sobre las prioridades de la web que cualquier lista de APIs.
Decidir con dos preguntas
Un plano solo vale lo que valga el procedimiento que permite recorrerlo, así que conviene reducirlo a algo mecánico. En la práctica el plano se opera con dos preguntas encadenadas, en este orden. ¿Necesito el valor antes de pintar el primer fotograma? Si la respuesta es sí, estás obligado al cuadrante síncrono del hilo principal, y por tanto a Web Storage, con todo lo que eso implica: dato diminuto, acotado y prescindible. Si es no, el cuadrante síncrono queda descartado sin más análisis.
¿Necesito acceder a una parte sin cargar el todo? Si la respuesta es sí, necesitas un almacén estructurado, y la conversación se reduce a IndexedDB o a un motor sobre OPFS. Si es no, y el dato es realmente pequeño, un almacén textual puede bastar. Casi todas las decisiones de almacenamiento de una aplicación real se resuelven con estas dos preguntas; el resto de criterios —cuota, desalojo, ergonomía de la biblioteca— solo desempata entre los candidatos que ya han sobrevivido.
Conviene fijarse en el orden, porque no es arbitrario. La primera pregunta es eliminatoria y muy rara vez se responde que sí: el conjunto de datos que de verdad se necesitan antes del primer fotograma es diminuto en cualquier aplicación honesta, y cuando alguien afirma que su lista es larga, lo que suele estar describiendo es un arranque mal diseñado, no un requisito. La segunda pregunta es de escala, y su respuesta cambia con el tiempo: un dato que hoy se lee entero sin problema puede necesitar acceso parcial dentro de un año, lo que significa que la respuesta debe darse pensando en el volumen previsible y no en el actual.
Aplicado con disciplina, este orden produce una arquitectura muy reconocible y bastante universal: una o dos claves en Web Storage para lo que decide el primer fotograma, la Cache API para todo lo que llega por HTTP, y un almacén estructurado en un Worker para absolutamente todo lo demás. Que casi todos los proyectos serios converjan en esa misma forma no es imitación: es lo que queda cuando se aplican las dos preguntas sin hacerse trampas.
Merece la pena ver por qué estos dos ejes, y no otros, ordenan el territorio: no son una taxonomía elegida por conveniencia didáctica, sino la sombra que proyectan las dos únicas restricciones duras bajo las que opera cualquier almacenamiento en un navegador. La primera es que el modelo de ejecución de la web tiene un hilo privilegiado que atiende simultáneamente al usuario y al dibujo de la pantalla, de modo que cualquier microsegundo que le quites se lo estás quitando literalmente a la percepción de fluidez de una persona; el eje síncrono frente a asíncrono no es más que la pregunta de si vas a cobrarle a ese hilo o a otro, y por eso su resolución correcta no fue eliminar la sincronía sino desplazarla a un hilo donde bloquear no le duele a nadie, que es exactamente la jugada que hacen los manejadores de acceso síncrono de OPFS dentro de un Worker. La segunda restricción es que la memoria del proceso es finita y la deserialización es proporcional al tamaño, de modo que un almacén que solo sabe devolverte una cadena te condena a materializar el conjunto entero para consultar un campo; el eje estructurado frente a texto plano es la pregunta de si el almacén sabe darte una parte, y su consecuencia no es de comodidad sino de escala, porque marca la frontera entre lo que crece linealmente con el tamaño de tus datos y lo que crece con el tamaño de tu consulta. Cuando comprendes que los ejes son estas dos restricciones y no seis APIs, ocurren dos cosas valiosas. Primero, dejas de necesitar memorizar el inventario: cualquier mecanismo nuevo que la plataforma añada en los próximos años se colocará solo en el plano en cuanto respondas dónde bloquea y qué forma devuelve. Segundo, y más importante, empiezas a leer las arquitecturas ajenas con criterio: entiendes por qué un motor local-first serio termina invariablemente en la misma configuración —el almacén en un Worker, sincronía dentro de él, estructura impuesta por un motor real, y solo mensajes cruzando hacia el hilo principal— y comprendes que esa convergencia no es una moda ni una imitación entre proyectos, sino la única esquina del plano donde ambas restricciones se satisfacen a la vez. Todo el resto del track construye sobre esa esquina.
- Toma una aplicación tuya y clasifica cada dato que persiste según las dos preguntas de la última sección. Anota en qué cuadrante debería estar y en cuál está.
- Mide el coste síncrono del clonado estructurado: escribe en IndexedDB un objeto pequeño y otro de varios megabytes, e instrumenta el tiempo consumido en el hilo que llama, no el de la promesa.
- Provoca deliberadamente la caducidad de una transacción de IndexedDB intercalando un
awaitajeno, y explica con el modelo de eventos por qué ocurre. - Argumenta por qué la plataforma nunca ha ofrecido un almacén síncrono y estructurado en el hilo principal, y qué habría pasado con las aplicaciones web si lo hubiera hecho.