IndexedDB : relations, structures avancées et bonnes pratiques
Ce troisième volet prolonge notre progression à travers IndexedDB. Après avoir exploré les lectures, modifications, transactions et accès par curseur dans Explorer et modifier les données dans IndexedDB : curseurs, accès directs et transactions, nous abordons maintenant des notions plus avancées : modèles complexes de données, associations logiques entre entités, fonctionnement offline et bonnes pratiques. IndexedDB peut aller bien au-delà d’un simple stockage local. Mais pour en tirer le meilleur parti, il faut comprendre ses spécificités.
Nous aborderons quatre volets clés :
- Travailler avec des objets complexes – Enregistrer des structures imbriquées, des tableaux, ou des identifiants personnalisés sans confusion.
- Simuler des relations entre stores – Tirer parti de clés logiques pour organiser des ensembles de données connectés, comme dans une base relationnelle.
- Études de cas – Explorer IndexedDB dans des scénarios concrets : PWA, fonctionnement offline, synchronisation distante.
- Limites et bonnes pratiques – Éviter les erreurs courantes, nettoyer les bases anciennes, rester compatible avec les navigateurs, ou explorer d’autres outils.
Chaque chapitre s’accompagnera d’exemples concrets et de conseils adaptés à un usage réel, que vous soyez en train de créer une application offline ou de fiabiliser un stockage longue durée.
Travailler avec des objets complexes
IndexedDB ne se limite pas à des paires clé/valeur simples. On peut y stocker des objets structurés contenant d’autres objets ou des tableaux. Cela permet de modéliser des données proches de celles d’une base relationnelle ou d’une API REST.
store.put({
id: 12,
title: "October File",
year: 1979,
group: { id: 4, name: "Die Krupps" },
tracks: [
{ title: "Track A", duration: 178 },
{ title: "Track B", duration: 195 }
]
});Cet objet sera stocké tel quel dans IndexedDB, sans besoin de sérialisation manuelle. Lors de la lecture, on retrouvera exactement la même structure.
Cependant, cela ne veut pas dire qu’on puisse faire des recherches directes sur les sous-propriétés. IndexedDB ne peut indexer que des chemins de propriété explicites. Pour interroger un champ imbriqué (comme group.name), il faut avoir défini un index sur ce chemin dès la création du store.
store.createIndex("groupName", "group.name", { unique: false });Cela permettra par exemple de chercher tous les albums dont le groupe s’appelle “Die Krupps”, même si cette information est nichée dans un objet.
Un autre cas fréquent concerne les tableaux. Si une propriété contient plusieurs valeurs (par exemple plusieurs genres ou mots-clés), l’indexation nécessite l’option multiEntry.
store.createIndex("keywordsIndex", "keywords", { multiEntry: true });Ainsi, chaque élément du tableau sera indexé individuellement. Une requête sur un mot-clé précis renverra alors tous les objets qui contiennent ce mot, même si ce n’est qu’un des éléments du tableau.
store.put({
id: 13,
title: "Nuclear Nightclub",
keywords: ["krautrock", "experimental", "cosmic"]
});IndexedDB devient alors un vrai mini-système de stockage structuré. Mais cela suppose une anticipation des besoins d’indexation, car ces chemins ne peuvent pas être modifiés une fois la base créée sans changer sa version.
Simuler des relations entre stores
IndexedDB ne propose pas de véritables relations comme dans une base SQL, mais il est possible de les simuler en structurant correctement ses données. Cela permet de modéliser des liens entre entités, comme un groupe et ses albums, ou un utilisateur et ses préférences.
Imaginons deux stores distincts : groups et albums. Chaque album pourrait contenir un identifiant (groupId) qui fait référence à une entrée du store groups. Ce lien n’est pas automatique, mais il peut être exploité dans notre logique applicative.
const album = {
id: 17,
title: "Rien",
year: 1972,
groupId: 3 // fait référence à un groupe existant dans le store "groups"
};
albumStore.put(album);Pour afficher les albums avec leur nom de groupe, il faudra effectuer deux lectures : l’une dans le store albums, puis une autre dans groups avec l’identifiant récupéré. Ce double accès peut se faire dans une même transaction ou via deux appels enchaînés.
const tx = db.transaction(["albums", "groups"], "readonly");
const albumStore = tx.objectStore("albums");
const groupStore = tx.objectStore("groups");
albumStore.get(17).onsuccess = function(event) {
const album = event.target.result;
groupStore.get(album.groupId).onsuccess = function(e) {
const group = e.target.result;
console.log(album.title + " - " + group.name);
};
};Cette logique permet de retrouver la structure relationnelle qu’on connaît dans d’autres systèmes, à condition de gérer manuellement les associations.
Il est également possible de croiser plusieurs albums avec leur groupe via des boucles et des objets temporaires, ou de générer des structures composites à l’affichage, mais cela dépend entièrement de la logique métier. IndexedDB ne fera pas ce travail à notre place.
Dans le chapitre suivant, nous explorerons des usages concrets de cette approche pour concevoir des interfaces fluides et pertinentes, y compris dans des contextes hors ligne.
Études de cas – Offline, PWA, cache, synchronisation asynchrone
L’un des atouts majeurs d’IndexedDB réside dans son usage hors ligne. En tant que base de données locale, elle permet à une application de rester fonctionnelle même sans connexion, en stockant les données côté navigateur. Ce fonctionnement est particulièrement adapté aux applications web progressives (PWA), qui visent une expérience fluide et résiliente.
Prenons le cas d’une application musicale. Chaque morceau ajouté à une playlist est enregistré localement dans IndexedDB. Lorsqu’une connexion réseau est disponible, une routine de synchronisation peut transmettre les ajouts récents vers un serveur distant, puis marquer ces morceaux comme « synchronisés ». Le store peut alors inclure un champ booléen synced ou un champ lastSynced pour suivre cet état.
const track = {
id: 14,
title: "Der Baum",
artist: "Grobschnitt",
album: "Jumbo",
synced: false
};
playlistStore.put(track);Une fois la synchronisation effectuée, il est essentiel de mettre à jour les données locales pour qu’elles reflètent leur nouvel état. Cela permet d’éviter les doublons, de prévenir une nouvelle tentative de synchronisation inutile, et de garder une trace précise des interactions entre l’application et le serveur distant. Dans notre cas, chaque morceau précédemment stocké avec synced: false doit être mis à jour en synced: true après confirmation du serveur. Cette opération, même si elle semble mineure, est au cœur de la logique de cohérence entre l’état local et l’état distant.
track.synced = true;
playlistStore.put(track);Pour réagir à la reprise de connexion, une routine peut s’appuyer sur l’événement online, déclenché automatiquement par le navigateur lorsque la connexion Internet est rétablie. Cela permet à l’application de relancer, sans action de l’utilisateur, les processus de synchronisation différée. C’est un levier précieux pour maintenir la cohérence des données sans dégrader l’expérience utilisateur.
window.addEventListener("online", () => {
const request = playlistStore.index("syncedIndex").openCursor(IDBKeyRange.only(false));
request.onsuccess = function(event) {
const cursor = event.target.result;
if (cursor) {
sendToServer(cursor.value).then(() => {
cursor.value.synced = true;
cursor.update(cursor.value);
cursor.continue();
});
}
};
});Cette logique peut aussi servir de cache. Lors d’une première visite en ligne, on récupère les données depuis le serveur et on les enregistre localement. Les prochaines visites liront depuis IndexedDB, en économisant des requêtes, sauf si une mise à jour est disponible. Ce pattern s’applique aussi aux playlists, morceaux, ou profils d’artiste. Cependant, même si les métadonnées d’un morceau (titre, artiste, durée) sont bien disponibles hors ligne, la lecture ne sera possible que si le fichier audio lui-même a été mis en cache par le navigateur via d’autres mécanismes (Service Worker, Cache API, etc.).
IndexedDB devient alors un maillon essentiel dans les stratégies de résilience des applications web, en conjuguant performance, continuité de service, et contrôle fin des données. Dans le chapitre suivant, nous verrons comment éviter les pièges courants et structurer proprement notre base pour en tirer le meilleur.
Nettoyer régulièrement les données
IndexedDB ne supprime rien par défaut. Il est donc essentiel de mettre en place des stratégies de nettoyage pour éviter d’accumuler des informations obsolètes ou inutiles (sessions expirées, brouillons anciens, contenus temporaires). Cela peut se faire en ajoutant un champ expiresAt lors de l’insertion des données, puis en programmant un balayage périodique qui supprime les enregistrements expirés. Une autre approche consiste à stocker un timestamp et à filtrer ou supprimer les données au moment de la lecture.
const now = Date.now();
const transaction = db.transaction('playlist', 'readwrite');
const store = transaction.objectStore('playlist');
const request = store.openCursor();
request.onsuccess = function (event) {
const cursor = event.target.result;
if (cursor) {
const record = cursor.value;
if (record.expiresAt && record.expiresAt < now) {
cursor.delete(); // Supprime les enregistrements expirés
}
cursor.continue();
}
};Il est aussi possible d’ajouter une telle vérification au démarrage de l’application ou après certaines opérations clés. L’objectif est de conserver une base légère, pertinente, et facile à synchroniser si nécessaire.
Supprimer automatiquement une base IndexedDB obsolète
Contrairement à un serveur distant, il n’existe pas de tâche planifiée (comme un CRON) côté navigateur. Mais on peut tout de même mettre en place une logique de suppression automatique, déclenchée par le code lui-même, selon certaines conditions.
L’API indexedDB.databases() permet de lister les bases existantes (dans les navigateurs compatibles comme Chrome ou Edge), appartenant au même domaine. On peut alors décider de supprimer celles qui ne sont plus nécessaires :
if ('databases' in indexedDB) {
indexedDB.databases().then((dbs) => {
dbs.forEach((db) => {
if (db.name === 'ancienneBase' && db.version < 2) {
indexedDB.deleteDatabase(db.name);
}
});
});
}On peut également stocker un champ de type lastUsed dans une base existante, et en déduire, au lancement de l’application, s’il faut supprimer certaines structures inutilisées depuis trop longtemps. Cela reste une gestion manuelle mais automatisable, que l’on peut insérer dans le code selon les besoins réels de notre application.
Limites et bonnes pratiques
IndexedDB offre des fonctionnalités puissantes pour le stockage structuré côté client, mais son usage efficace suppose de connaître ses limites et de respecter quelques règles de prudence. Dans ce dernier chapitre, nous revenons sur les pièges fréquents, les comportements implicites à anticiper, et les stratégies recommandées pour garantir une base saine et durable.
- Attention aux erreurs silencieuses : certaines opérations, comme
put()avec des clés incorrectes ou des index en doublon, ne déclenchent pas toujours des erreurs visibles si elles ne sont pas correctement interceptées. Il est donc essentiel d’attacher systématiquement des gestionnairesonsuccessetonerror, même pour des appels simples. - Gestion du volume et des performances : IndexedDB n’est pas conçu pour contenir des gigaoctets de données. En théorie, la limite dépend du navigateur, du système et des permissions. En pratique, mieux vaut rester dans des volumes raisonnables, typiquement entre 50 et 100 Mo selon les navigateurs. Il est aussi recommandé de regrouper les écritures dans une même transaction plutôt que d’enchaîner les appels séparés.
- Nettoyer régulièrement : contrairement à un serveur distant, les données IndexedDB ne sont pas automatiquement purgées. Pensez à définir des politiques de nettoyage (données expirées, sessions anciennes, etc.) pour éviter d’alourdir inutilement l’application. Cela peut se faire via des clés de type
timestampou un champexpiresAtassocié à des routines de vérification. - Prendre en compte les mises à jour de structure : toute modification de la structure (ajout d’un store, d’un index…) nécessite une montée de version via
onupgradeneeded. Cela implique de penser à la compatibilité descendante ou à la migration des données, en particulier si l’application est utilisée sur plusieurs appareils ou navigateurs. - Être explicite dans les schémas : même si IndexedDB n’impose pas de schéma rigide, il est recommandé de rester clair sur les clés primaires, les index créés, et les champs obligatoires attendus. Cela facilitera la maintenance, l’évolution, et la documentation du projet, notamment dans les contextes collaboratifs.
- Associer IndexedDB à d’autres API : pour des usages avancés (lecture hors ligne de médias, synchronisation d’état, gestion des versions…), IndexedDB peut être combiné à d’autres technologies du navigateur, comme les Service Workers, la Cache API ou le File System Access. Chaque outil ayant son propre rôle.
IndexedDB ne remplace pas une base distante, mais permet de construire des ponts robustes entre l’interface utilisateur et les données, même dans des contextes intermittents ou déconnectés. À condition d’en comprendre les ressorts, elle devient une brique stratégique dans l’architecture d’une application moderne. Dans notre prochain article, nous aborderons justement ces cas complexes et la manière d’organiser une base efficace et relationnelle.
