Bonnes pratiques d'utilisation de la base de données indexée

Découvrez les bonnes pratiques pour synchroniser l'état d'une application entre IndexedDB, une bibliothèque de gestion d'état populaire.

Lorsqu'un utilisateur charge pour la première fois un site Web ou une application, la création de l'état initial de l'application servant à afficher l'UI implique souvent une charge de travail considérable. Par exemple, l'application doit parfois authentifier l'utilisateur côté client, puis envoyer plusieurs requêtes API avant de disposer de toutes les données à afficher sur la page.

Le stockage de l'état de l'application dans IndexedDB peut être un excellent moyen d'accélérer le temps de chargement en cas de visites répétées. L'application peut ensuite se synchroniser avec tous les services d'API en arrière-plan et mettre à jour l'UI avec de nouvelles données en différé, à l'aide d'une stratégie stale-while-revalidate.

IndexedDB permet également de stocker du contenu généré par l'utilisateur, soit en tant que stockage temporaire avant son importation sur le serveur, soit en tant que cache côté client de données distantes, ou, bien entendu, les deux.

Cependant, lors de l'utilisation d'IndexedDB, de nombreux points importants sont à prendre en compte et peuvent ne pas être immédiatement évidents pour les développeurs qui débutent avec les API. Cet article répond aux questions fréquentes et aborde certains des points les plus importants à garder à l'esprit lors de la persistance des données dans IndexedDB.

Assurer la prévisibilité de votre application

Une grande partie des complexités liées à IndexedDB sont dues au fait qu'il existe de nombreux facteurs sur lesquels vous (le développeur) n'avez aucun contrôle. Cette section explore la plupart des problèmes à garder à l'esprit lorsque vous utilisez IndexedDB.

Toutes les plates-formes ne peuvent pas être stockées dans IndexedDB

Si vous stockez des fichiers volumineux générés par l'utilisateur, tels que des images ou des vidéos, vous pouvez essayer de les stocker en tant qu'objets File ou Blob. Cette opération fonctionnera sur certaines plates-formes, mais échouera sur d'autres. Safari sur iOS, en particulier, ne peut pas stocker de Blob dans IndexedDB.

Heureusement, il n'est pas trop difficile de convertir un Blob en ArrayBuffer, et inversement. Le stockage des valeurs ArrayBuffer dans IndexedDB est très bien pris en charge.

N'oubliez pas, cependant, qu'une Blob possède un type MIME, contrairement à un ArrayBuffer. Vous devez stocker le type avec le tampon pour effectuer la conversion correctement.

Pour convertir un ArrayBuffer en Blob, il vous suffit d'utiliser le constructeur Blob.

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

Dans l'autre sens, le processus est un peu plus complexe et asynchrone. Vous pouvez utiliser un objet FileReader pour lire l'objet blob en tant que ArrayBuffer. Une fois la lecture terminée, un événement loadend est déclenché sur le lecteur. Vous pouvez encapsuler ce processus dans un Promise comme ceci:

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);
  });
}

Échec de l'écriture dans l'espace de stockage

Des erreurs d'écriture dans IndexedDB peuvent se produire pour diverses raisons et, dans certains cas, hors de votre contrôle en tant que développeur. Par exemple, certains navigateurs n'autorisent actuellement pas l'écriture dans IndexedDB en mode de navigation privée. Il est également possible qu'un utilisateur se trouve sur un appareil presque saturé, ce qui vous empêche de stocker quoi que ce soit dans le navigateur.

C'est pourquoi il est essentiel que vous implémentez toujours la gestion des erreurs appropriée dans votre code IndexedDB. Cela signifie également qu'il est généralement judicieux de conserver l'état de l'application en mémoire (en plus de le stocker), afin que l'interface utilisateur reste intacte en mode navigation privée ou lorsque l'espace de stockage n'est pas disponible (même si certaines des autres fonctionnalités de l'application qui nécessitent de l'espace de stockage ne fonctionnent pas).

Vous pouvez détecter les erreurs dans les opérations IndexedDB en ajoutant un gestionnaire d'événements pour l'événement error chaque fois que vous créez un objet IDBDatabase, IDBTransaction ou IDBRequest.

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

Les données stockées peuvent avoir été modifiées ou supprimées par l'utilisateur.

Contrairement aux bases de données côté serveur pour lesquelles vous pouvez restreindre les accès non autorisés, les bases de données côté client sont accessibles aux extensions de navigateur et aux outils pour les développeurs, et peuvent être effacées par l'utilisateur.

Bien qu'il puisse être rare pour les utilisateurs de modifier leurs données stockées localement, il est assez courant qu'ils les effacent. Il est important que votre application puisse gérer ces deux cas sans erreur.

Les données stockées sont peut-être obsolètes

Comme dans la section précédente, même si l'utilisateur n'a pas modifié les données lui-même, il est également possible que les données qu'il a stockées aient été écrites par une ancienne version de votre code, éventuellement une version contenant des bugs.

IndexedDB est compatible avec les versions de schéma et les mises à niveau via sa méthode IDBOpenDBRequest.onupgradeneeded(). Toutefois, vous devez écrire votre code de mise à niveau de sorte qu'il puisse gérer l'utilisateur provenant d'une version précédente (y compris une version présentant un bug).

Les tests unitaires peuvent être très utiles dans ce contexte, car il est souvent impossible de tester manuellement tous les chemins et cas de mise à niveau possibles.

Préserver les performances de votre application

L'une des principales fonctionnalités d'IndexedDB est son API asynchrone, mais ne vous laissez pas croire que vous n'avez pas à vous soucier des performances lorsque vous l'utilisez. Dans de nombreux cas, une utilisation incorrecte peut toujours bloquer le thread principal, ce qui peut entraîner des à-coups et une absence de réponse.

En règle générale, le nombre de lectures et d'écritures dans IndexedDB ne doit pas dépasser la limite requise pour les données consultées.

Bien qu'IndexedDB permette de stocker des objets volumineux et imbriqués sous la forme d'un seul enregistrement (ce qui est certes assez pratique du point de vue des développeurs), cette pratique doit être évitée. En effet, lorsque IndexedDB stocke un objet, il doit d'abord créer un clone structuré de cet objet, puis le processus de clonage structuré se produit sur le thread principal. Plus l'objet est grand, plus le temps de blocage est long.

Cela pose certains problèmes lors de la planification de la persistance de l'état de l'application dans IndexedDB, car la plupart des bibliothèques de gestion d'état populaires (telles que Redux) gèrent l'intégralité de votre arborescence d'états comme un seul objet JavaScript.

Bien que la gestion de l'état de cette manière présente de nombreux avantages (par exemple, elle facilite le raisonnement et le débogage de votre code). Bien que le stockage complet de l'arborescence d'état sous la forme d'un seul enregistrement dans IndexedDB puisse être tentant et pratique, effectuer cette opération après chaque modification (même en cas de ralentissement/rejet) entraîne un blocage inutile du thread principal, augmente les risques d'erreurs d'écriture et, dans certains cas, peut entraîner un plantage du navigateur.

Au lieu de stocker l'arborescence d'état complète dans un seul enregistrement, vous devez la diviser en enregistrements individuels et ne mettre à jour que les enregistrements qui changent réellement.

Il en va de même si vous stockez des éléments volumineux comme des images, de la musique ou des vidéos dans IndexedDB. Stockez chaque élément avec sa propre clé plutôt que dans un objet plus grand, afin de pouvoir récupérer les données structurées sans payer les frais liés à la récupération du fichier binaire.

Comme dans la plupart des bonnes pratiques, il ne s'agit pas d'une règle "tout ou rien". Dans les cas où il n'est pas possible de diviser un objet d'état et d'écrire simplement l'ensemble de modifications minimal, il est préférable de diviser les données en sous-arborescences et de n'écrire que celles-ci, plutôt que d'écrire l'intégralité de l'arbre d'état. Des améliorations minimes valent mieux qu'aucune amélioration.

Enfin, vous devez toujours mesurer l'impact sur les performances du code que vous écrivez. Bien que les petites écritures sur IndexedDB soient meilleures que les grandes écritures, cela n'a d'importance que si les écritures dans IndexedDB effectuées par votre application entraînent en fait des tâches longues qui bloquent le thread principal et nuisent à l'expérience utilisateur. Il est important d'effectuer des mesures afin de comprendre l'objectif de l'optimisation.

Conclusions

Les développeurs peuvent exploiter les mécanismes de stockage client tels qu'IndexedDB pour améliorer l'expérience utilisateur de leur application en non seulement en conservant l'état d'une session, mais également en réduisant le temps de chargement de l'état initial lors des visites répétées.

Bien que l'utilisation appropriée d'IndexedDB puisse améliorer considérablement l'expérience utilisateur, son utilisation incorrecte ou ne pas gérer les cas d'erreur peut entraîner des problèmes d'application et des utilisateurs insatisfaits.

Étant donné que le stockage client implique de nombreux facteurs que vous ne contrôlez pas, il est essentiel que votre code soit bien testé et qu'il gère correctement les erreurs, même celles qui semblent peu probables au départ.