Prácticas recomendadas para usar IndexedDB

Conoce las prácticas recomendadas para sincronizar el estado de la aplicación entre IndexedDB y bibliotecas populares de administración de estado.

Cuando un usuario carga un sitio web o una aplicación por primera vez, suele implicar bastante trabajo en la construcción del estado inicial de la aplicación que se utiliza para renderizar la IU. Por ejemplo, a veces, la app necesita autenticar el lado del cliente del usuario y, luego, realizar varias solicitudes a la API antes de tener todos los datos que necesita mostrar en la página.

Almacenar el estado de la aplicación en IndexedDB puede ser una excelente manera de acelerar el tiempo de carga de las visitas repetidas. Luego, la app puede sincronizarse con cualquier servicio de API en segundo plano y actualizar la IU con datos nuevos de forma diferida mediante una estrategia de inactividad al momento de revalidar.

Otro buen uso de IndexedDB es almacenar contenido generado por usuarios, ya sea como un almacenamiento temporal antes de que se suba al servidor o como una caché de datos remotos del cliente, o, por supuesto, ambos.

Sin embargo, cuando se utiliza IndexedDB, hay muchos aspectos importantes que se deben tener en cuenta que pueden no ser evidentes de inmediato para los desarrolladores que son nuevos en las APIs. En este artículo, se responden preguntas comunes y se analizan algunos de los aspectos más importantes que debes tener en cuenta cuando se conservan datos en IndexedDB.

Cómo hacer que tu app sea predecible

Muchas de las complejidades en torno a IndexedDB se originan en el hecho de que hay muchos factores sobre los que tú (el desarrollador) no tienes control. En esta sección, se exploran muchos de los problemas que debes tener en cuenta cuando trabajas con IndexedDB.

No todo se puede almacenar en IndexedDB en todas las plataformas

Si almacenas archivos grandes generados por el usuario, como imágenes o videos, puedes intentar almacenarlos como objetos File o Blob. Esto funcionará en algunas plataformas, pero fallará en otras. En particular, Safari en iOS no puede almacenar elementos Blob en IndexedDB.

Afortunadamente, no es demasiado difícil convertir un Blob en un ArrayBuffer, y viceversa. El almacenamiento de ArrayBuffer en IndexedDB es muy compatible.

No obstante, recuerda que una Blob tiene un tipo de MIME, mientras que una ArrayBuffer no. Deberás almacenar el tipo junto con el búfer para realizar la conversión de forma correcta.

Para convertir un ArrayBuffer en un Blob, solo debes usar el constructor Blob.

function arrayBufferToBlob(buffer, type) {
  return new Blob([buffer], { type: type });
}

La otra dirección es un poco más compleja y es un proceso asíncrono. Puedes usar un objeto FileReader para leer el BLOB como un ArrayBuffer. Cuando finaliza la lectura, se activa un evento loadend en el lector. Puedes unir este proceso en un Promise de la siguiente manera:

function blobToArrayBuffer(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.addEventListener('loadend', () => {
      resolve(reader.result);
    });
    reader.addEventListener('error', reject);
    reader.readAsArrayBuffer(blob);
  });
}

Es posible que falle la escritura en el almacenamiento

Los errores que se producen cuando se escribe en IndexedDB pueden ocurrir por varios motivos y, en algunos casos, están fuera de tu control como desarrollador. Por ejemplo, algunos navegadores no permiten escribir en IndexedDB cuando se utiliza el modo de navegación privada. También existe la posibilidad de que un usuario esté usando un dispositivo que está casi sin espacio en el disco, y el navegador no te permitirá guardar nada.

Por lo tanto, es muy importante que siempre implementes el manejo adecuado de errores en tu código de IndexedDB. Esto también significa que, en general, es una buena idea mantener el estado de la aplicación en la memoria (además de almacenarlo), para que la IU no se dañe cuando se ejecuta en el modo de navegación privada o cuando no hay espacio de almacenamiento disponible (incluso si no pueden usarse algunas de las otras funciones de la app que requieren almacenamiento).

Puedes detectar errores en las operaciones de IndexedDB si agregas un controlador de eventos para el evento error cada vez que crees un objeto IDBDatabase, IDBTransaction o IDBRequest.

const request = db.open('example-db', 1);
request.addEventListener('error', (event) => {
  console.log('Request error:', request.error);
};

El usuario puede haber modificado o borrado los datos almacenados

A diferencia de las bases de datos del servidor, en las que puedes restringir el acceso no autorizado, las bases de datos del cliente son accesibles para las extensiones del navegador y las herramientas para desarrolladores, y el usuario puede borrarlas.

Si bien puede ser poco común que los usuarios modifiquen sus datos almacenados de forma local, es bastante común que los usuarios los borren. Es importante que tu aplicación pueda manejar ambos casos sin errores.

Es posible que los datos almacenados estén desactualizados

Al igual que en la sección anterior, incluso si el usuario no modificó los datos por su cuenta, también es posible que los datos que tiene almacenados se hayan escrito con una versión anterior del código, que puede contener errores.

IndexedDB tiene compatibilidad integrada con versiones de esquema y actualizaciones a través del método IDBOpenDBRequest.onupgradeneeded(). Sin embargo, aún debes escribir tu código de actualización de manera que pueda controlar el usuario que proviene de una versión anterior (incluida una versión con un error).

Las pruebas de unidades pueden ser muy útiles en este caso, ya que, a menudo, no es factible probar de forma manual todos los casos y las rutas de actualización posibles.

Cómo mantener el rendimiento de tu app

Una de las funciones clave de IndexedDB es su API asíncrona, pero no dejes que eso te engañe para que pienses que no necesitas preocuparte por el rendimiento cuando la usas. Hay varios casos en los que el uso inadecuado puede bloquear el subproceso principal, lo que puede generar bloqueos y faltas de respuesta.

Como regla general, las operaciones de lectura y escritura en IndexedDB no deben ser más grandes que el requerido para los datos a los que se accede.

Si bien IndexedDB permite almacenar objetos grandes y anidados como un solo registro (y, ciertamente, hacerlo es bastante conveniente desde la perspectiva de un desarrollador), se debe evitar esta práctica. Esto se debe a que, cuando IndexedDB almacena un objeto, primero debe crear una clonación estructurada de ese objeto, y el proceso de clonación estructurada ocurre en el subproceso principal. Cuanto más grande sea el objeto, mayor será el tiempo de bloqueo.

Esto presenta algunos desafíos a la hora de planificar cómo conservar el estado de la aplicación en IndexedDB, ya que la mayoría de las bibliotecas de administración de estado populares (como Redux) administran todo el árbol de estado como un solo objeto JavaScript.

Si bien administrar el estado de esta manera tiene muchos beneficios (p.ej., hace que tu código sea fácil de razonar y depurar), y aunque simplemente almacenar todo el árbol de estado como un solo registro en IndexedDB puede ser tentador y conveniente, hacer esto después de cada cambio (incluso si se limita o se devuelve) generará un bloqueo innecesario del subproceso principal, incluso aumentará la probabilidad de que el navegador deje de responder y, en algunos casos, que falle el navegador.

En lugar de almacenar todo el árbol de estado en un solo registro, debes dividirlo en registros individuales y actualizar solo los registros que cambian en realidad.

Lo mismo sucede si almacenas elementos grandes, como imágenes, música o videos, en IndexedDB. Almacena cada elemento con su propia clave en lugar de dentro de un objeto más grande, de modo que puedas recuperar los datos estructurados sin pagar el costo de recuperar también el archivo binario.

Como ocurre con la mayoría de las prácticas recomendadas, esta no es una regla de todo o nada. En los casos en los que no sea factible dividir un objeto de estado y solo escribir el conjunto de cambios mínimo, es preferible dividir los datos en subárboles y solo escribirlos en lugar de escribir siempre todo el árbol de estado. Algunas mejoras son mejores que ninguna.

Por último, siempre debes medir el impacto en el rendimiento del código que escribes. Si bien es cierto que las escrituras pequeñas en IndexedDB tendrán un mejor rendimiento que las grandes, esto solo es importante si las escrituras en IndexedDB que realiza tu aplicación generan tareas largas que bloquean el subproceso principal y degradan la experiencia del usuario. Es importante realizar mediciones para que comprendas por qué estás optimizando.

Conclusiones

Los desarrolladores pueden aprovechar los mecanismos de almacenamiento del cliente, como IndexedDB, para mejorar la experiencia del usuario de su aplicación no solo mediante la persistencia del estado en todas las sesiones, sino también la disminución del tiempo que se tarda en cargar el estado inicial en visitas repetidas.

Si bien usar IndexedDB de forma correcta puede mejorar drásticamente la experiencia del usuario, usarlo de forma incorrecta o no manejar los casos de error puede generar apps dañadas y usuarios descontentos.

Dado que el almacenamiento del cliente involucra muchos factores fuera de tu control, es fundamental que tu código esté bien probado y maneje correctamente los errores, incluso aquellos que al principio puedan parecer poco probables.