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

Découvrez les bonnes pratiques de synchronisation de l'état d'une application entre IndexedDB et des bibliothèques de gestion d'état populaires.

Lorsqu'un utilisateur charge pour la première fois un site Web ou une application, la construction de l'état initial de l'application utilisé pour afficher l'UI implique souvent une charge importante de travail. Par exemple, il arrive que l'application doive authentifier l'utilisateur côté client, puis effectuer plusieurs requêtes API avant de disposer de toutes les données nécessaires à l'affichage 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 des visites répétées. L'application peut ensuite se synchroniser avec n'importe quel service d'API en arrière-plan et mettre à jour l'UI avec de nouvelles données de manière différée, à l'aide d'une stratégie de type stale-while-revalidate.

Un autre bon usage d'IndexedDB consiste à 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 sûr, les deux.

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

Préserver la prévisibilité de votre application

Une grande partie de la complexité liée à IndexedDB vient du fait que vous (le développeur) n'avez aucun contrôle sur de nombreux facteurs. Cette section aborde de nombreux problèmes que vous devez garder à l'esprit lorsque vous travaillez avec 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. Cela fonctionnera sur certaines plates-formes, mais échouera sur d'autres. Sur iOS, Safari 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 de ArrayBuffer dans IndexedDB est très bien pris en charge.

Cependant, n'oubliez pas qu'un Blob a un type MIME, contrairement à un ArrayBuffer. Vous devrez 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 });
}

L'autre direction est légèrement plus complexe et est un processus asynchrone. Vous pouvez utiliser un objet FileReader pour lire le 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 suit:

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. Dans certains cas, ces raisons échappent à votre contrôle en tant que développeur. Par exemple, certains navigateurs n'autorisent actuellement pas l'écriture sur IndexedDB en mode de navigation privée. Il est également possible qu'un utilisateur se trouve sur un appareil qui manque presque d'espace disque, et que le navigateur vous empêche de stocker quoi que ce soit.

C'est pourquoi il est essentiel de toujours mettre en œuvre une gestion des erreurs appropriée dans votre code IndexedDB. Cela signifie également qu'il est généralement recommandé de conserver l'état de l'application en mémoire (en plus de le stocker), afin que l'interface utilisateur reste fluide 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 du 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, dans lesquelles vous pouvez limiter les accès non autorisés, les bases de données côté client sont accessibles aux extensions du navigateur et aux outils de développement, et peuvent être effacées par l'utilisateur.

Bien qu'il soit rare que les utilisateurs modifient 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 stockées dans l'espace de stockage aient été écrites par une ancienne version de votre code, éventuellement par une version comportant des bugs.

IndexedDB est compatible avec les versions de schéma et les mises à niveau via sa méthode IDBOpenDBRequest.onupgradeneeded(). Toutefois, vous devez toujours é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 cas, 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. Toutefois, ne vous laissez pas croire que vous n'avez pas à vous soucier des performances lorsque vous l'utilisez. Dans un certain nombre de cas, une utilisation inappropriée 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, les lectures et les écritures dans IndexedDB ne doivent pas dépasser la limite requise pour les données consultées.

Même si IndexedDB permet de stocker des objets volumineux et imbriqués sous la forme d'un enregistrement unique (ce qui est certes très pratique pour les 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, et le processus de clonage structuré a lieu sur le thread principal. Plus l'objet est grand, plus le temps de blocage sera long.

Cela présente des difficultés 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 l'arborescence d'état comme un seul objet JavaScript.

Bien que la gestion de l'état présente de nombreux avantages (par exemple, elle facilite le raisonnement et le débogage de votre code). Stocker l'intégralité de l'arborescence d'état sous la forme d'un seul enregistrement dans IndexedDB peut être tentant et pratique. Toutefois, le faire après chaque modification (même en cas de limitation ou de blocage) entraînera un blocage inutile du thread principal, mais cela augmentera le risque d'erreurs d'écriture ou, dans certains cas, le plantage du navigateur.

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

Il en va de même si vous stockez des éléments volumineux tels que 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 volumineux, afin de pouvoir récupérer les données structurées sans devoir payer les frais liés à la récupération du fichier binaire.

Comme pour la plupart des bonnes pratiques, il ne s'agit pas d'une règle du 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 toujours préférable de diviser les données en sous-arborescences et de n'écrire que celles-ci, plutôt que de toujours écrire l'arborescence d'état complète. Il vaut mieux qu'il y ait peu d'améliorations plutôt que de ne rien améliorer.

Enfin, vous devez toujours mesurer l'impact sur les performances du code que vous écrivez. Bien qu'il soit vrai que les petites écritures sur IndexedDB fonctionnent mieux que les écritures volumineuses, cela n'a d'importance que si les écritures sur IndexedDB effectuées par votre application entraînent réellement de longues tâches qui bloquent le thread principal et dégradent l'expérience utilisateur. Il est important de les mesurer afin de comprendre ce pour quoi vous optimisez.

Conclusions

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

Bien qu'utiliser IndexedDB correctement puisse améliorer considérablement l'expérience utilisateur, une utilisation incorrecte ou l'échec de la gestion des cas d'erreur peut entraîner des applications défectueuses et des utilisateurs mécontents.

É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 gère correctement les erreurs, même celles qui semblent peu probables au départ.