Prácticas recomendadas para usar IndexedDB

Conoce las prácticas recomendadas para sincronizar el estado de la aplicación entre IndexedDB, una popular biblioteca de administración de estados.

Cuando un usuario carga por primera vez un sitio web o una aplicación, suele haber una gran cantidad de trabajo involucrado en la construcción del estado inicial de la aplicación que se utiliza para renderizar la IU. Por ejemplo, en ocasiones, la app necesita autenticar al usuario del lado del cliente y, luego, realizar varias solicitudes a la API antes de que tenga todos los datos que necesita para mostrarse 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 manera diferida, a través de una estrategia de inactividad durante la revalidación.

Otro buen uso de IndexedDB es almacenar contenido generado por usuarios, ya sea como almacenamiento temporal antes de subirlo al servidor, como caché del cliente de datos remotos o, por supuesto, en ambos casos.

Sin embargo, cuando se usa IndexedDB, se deben tener en cuenta muchos aspectos importantes que pueden no ser evidentes de inmediato para los desarrolladores que son nuevos en el uso de 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 los datos en IndexedDB.

Haz que tu app sea predecible

Muchas de las complejidades de IndexedDB surgen de la existencia de tantos factores sobre los que el desarrollador no tiene 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. Safari en iOS, en particular, no puede almacenar Blob en IndexedDB.

Afortunadamente, no es muy difícil convertir Blob en ArrayBuffer, y viceversa. Se admite muy bien el almacenamiento de ArrayBuffer en IndexedDB.

Sin embargo, recuerda que un Blob tiene un tipo de MIME, mientras que un 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, simplemente usa 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 cuando se escribe en IndexedDB pueden ocurrir por diversos motivos y, en algunos casos, están fuera de tu control como desarrollador. Por ejemplo, actualmente, algunos navegadores no permiten escribir en IndexedDB en el modo de navegación privada. También existe la posibilidad de que un usuario esté usando un dispositivo que casi no tiene espacio en el disco y el navegador te impida almacenar contenido.

Por ello, 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 buena idea mantener el estado de la aplicación en la memoria (además de almacenarla), para que la IU no falle cuando se ejecute en el modo de navegación privada o cuando no haya espacio de almacenamiento disponible (incluso si no funcionarán algunas de las otras funciones de la app que requieren almacenamiento).

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

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

Es posible que el usuario haya 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 extensiones del navegador y las herramientas para desarrolladores pueden acceder a las bases de datos del cliente, 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 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, también es posible que los datos que tiene almacenados estén escritos en una versión anterior de tu código, posiblemente una versión con errores.

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

Las pruebas de unidades pueden ser muy útiles aquí, ya que, con frecuencia, no es posible probar de forma manual todas las rutas de acceso y los casos de actualización posibles.

Cómo mantener el rendimiento de tu app

Una de las características clave de IndexedDB es su API asíncrona, pero no dejes que eso te engañe y piense que no necesitas preocuparte por el rendimiento cuando la usas. Hay una serie de casos en los que el uso inadecuado aún puede bloquear el subproceso principal, lo que puede provocar bloqueos y falta de respuesta.

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

Si bien IndexedDB permite almacenar objetos grandes y anidados como un solo registro (y, desde la perspectiva del desarrollador, esto es muy conveniente), 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 se realiza en el subproceso principal. Cuanto más grande sea el objeto, más tiempo será el bloqueo.

Esto presenta algunos desafíos cuando se planifica cómo conservar el estado de la aplicación en IndexedDB, ya que la mayoría de las bibliotecas de administración de estados populares (como Redux) funcionan mediante la administración de todo el árbol de estados como un solo objeto de 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, si bien almacenar todo el árbol de estados como un solo registro en IndexedDB puede ser tentador y conveniente, hacer esto después de cada cambio (incluso si se limita o anula) generará un bloqueo innecesario del subproceso principal, y aumentará la probabilidad de que la pestaña del navegador falle o, en algunos casos, falle.

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

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 el archivo binario.

Como en 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 posible 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 antes que escribir todo el árbol de estado. Pequeñas mejoras son mejores que ninguna mejora.

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 realmente generan tareas largas que bloquean el subproceso principal y degradan la experiencia del usuario. Es importante realizar mediciones para comprender qué optimizaciones estás realizando.

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 reduciendo el tiempo que lleva cargar el estado inicial en visitas repetidas.

Si bien el uso correcto de IndexedDB puede mejorar drásticamente la experiencia del usuario, su uso incorrecto o no manejar los casos de error puede generar apps dañadas y usuarios insatisfechos.

Dado que el almacenamiento del cliente involucra muchos factores fuera de tu control, es fundamental que tu código esté bien probado y maneje de forma adecuada los errores, incluso aquellos que en un principio parezca poco probable que ocurran.