Comprendre IndexedDB : une base de données locale en JavaScript
Lorsque nous utilisons localStorage ou sessionStorage, comme présenté dans l’article Stockage local en JavaScript : bien utiliser localStorage et sessionStorage, nous travaillons avec des données simples et limitées : des paires clé/valeur en texte, pratiques mais rapidement insuffisantes.
Que faire lorsque nous devons stocker des objets entiers, conserver des milliers d’entrées ou fonctionner hors ligne ? IndexedDB répond précisément à ces besoins. Conçu comme une base de données locale intégrée aux navigateurs modernes, il ouvre la voie à un stockage plus structuré, asynchrone et robuste. Loin d’être un outil réservé aux experts, il s’impose comme la solution logique dès qu’un projet dépasse les limites des stockages traditionnels.
Les grands principes d’IndexedDB
IndexedDB n’est pas un simple tiroir à données : c’est une vraie base de données intégrée au navigateur, capable de gérer des structures complexes et de gros volumes. Contrairement à localStorage, elle fonctionne de manière asynchrone, sans bloquer la page pendant les lectures ou les écritures.
Plutôt que de manipuler du texte brut, IndexedDB permet de stocker directement des objets JavaScript. Si l’on imagine une application de carnet de bord, on peut y enregistrer chaque note avec sa date, ses tags, son contenu… sans rien convertir manuellement :
const album = {
title: 'Tago Mago',
artist: 'Can',
year: 1971,
tags: ['expérimental', 'psychédélique', 'avant-garde']
};
store.add(album);Ces objets sont regroupés dans des object stores, l’équivalent des tables dans une base classique, mais sans format rigide.
Autre particularité : tout fonctionne par événements. Lorsqu’on ouvre la base, qu’on ajoute une donnée ou qu’on en lit une, on écoute les réponses du navigateur avec des événements comme onsuccess ou onerror. Ce fonctionnement peut surprendre, mais il permet d’enchaîner les opérations sans ralentir le reste du site :
const request = indexedDB.open('maBase', 1);
request.onsuccess = (event) => {
const db = event.target.result;
console.log('Base ouverte avec succès');
};
request.onerror = () => {
console.warn('Erreur lors de l’ouverture de la base');
};IndexedDB est pris en charge par tous les navigateurs modernes. Il existe toutefois quelques variations selon les versions, notamment lors de la création ou la mise à jour d’une base. D’où l’intérêt de toujours tester son code dans les environnements visés.
Portée et cycle de vie d’une base IndexedDB
IndexedDB prend tout son sens dans des applications web qui doivent continuer à fonctionner sans connexion. Une PWA, par exemple, peut stocker localement des données essentielles dès la première visite, pour rester utilisable hors ligne ou dans des zones à faible réseau.
Une base IndexedDB est stockée localement dans le navigateur de l’utilisateur. Elle ne circule pas, ne se synchronise pas, et ne se partage pas entre différents navigateurs. Si l’utilisateur change de navigateur (de Firefox à Chrome, par exemple) ou de machine, la base n’existe tout simplement pas dans ce nouveau contexte. Il faudra alors la recréer, structure et contenu compris. Généralement de façon ciblée, avec un sous-ensemble de données suffisant pour fonctionner localement, sans chercher à reproduire toute la base distante.
La base est également isolée par origine : un même script utilisé sur deux domaines différents, ou même sur deux versions (http et https) d’un même site, n’aura pas accès à la même base. Chaque combinaison domaine + protocole + port + navigateur possède donc son propre espace de stockage.
La gestion des versions joue un rôle central dans la structure de cette base. IndexedDB ne conserve qu’une seule version active : dès qu’un script demande une version supérieure à celle déjà installée, ou pas, onupgradeneeded est déclenché et la structure est mise à jour. La version antérieure est écrasée, il ne s’agit pas d’un historique ni d’un système de migration parallèle. Dans la pratique, il n’est pas nécessaire de vérifier manuellement cette version : c’est l’appel à indexedDB.open() qui gère tout. Si la version demandée est différente de celle déjà installée, le navigateur déclenche alors de manière automatique l’événement onupgradeneeded. C’est là que l’on peut adapter et faire évoluer la structure de la base.
Le numéro de version passé à open() sert donc à signaler à IndexedDB l’état attendu de la base. S’il est plus grand que celui enregistré, une mise à jour s’enclenche ; sinon, la base est ouverte directement. Ce mécanisme permet de faire évoluer la structure (ajout de stores, d’index, changement de clé…) sans avoir à recharger toutes les données ou reconstruire manuellement la base. Cela garantit que, quelle que soit la version de départ, la base atteindra l’état attendu par la version actuelle du code.
// Si la base est en version 0 (nouvelle installation)
// → créer les stores de base (ex. albums)
// Si la base est en version 1
// → ajouter les nouveautés introduites dans la version 2 (ex. store 'groups')
// Si la base est en version 2
// → appliquer les évolutions de la version 3 (ex. ajout d’un index, adaptation de clé…)
// Et ainsi de suite…Créer une base et un object store
Tout commence par une tentative d’ouverture. Lorsqu’on utilise indexedDB.open(), on demande au navigateur d’ouvrir ou de créer une base locale, en lui fournissant un nom et un numéro de version. Si la base n’existe pas, elle est créée. Si elle existe déjà mais avec une version différente, onupgradeneeded est déclenché pour permettre les ajustements nécessaires.
const request = indexedDB.open('krautDB', 1);Dans cette commande, 'krautDB' est le nom choisi pour la base, et 1 correspond à la version cible de l’application. Elle permet de dire au navigateur “voici l’état attendu de la base à ce stade de l’application”. L’événement onupgradeneeded est alors l’endroit où l’on prépare la structure de cette base :
request.onupgradeneeded = (event) => {
const db = event.target.result;
db.createObjectStore('albums', { keyPath: 'id', autoIncrement: true });
};Ici, on crée un object store nommé 'albums', qui servira à stocker les enregistrements. Chaque album sera identifié par un champ id généré automatiquement. Cela ne signifie pas que seuls les identifiants seront stockés : on pourra y enregistrer des objets complets contenant par exemple un titre, une année, un groupe, des tags, etc. Cette structure est définie une seule fois, lors de la création initiale ou d’une mise à jour future. Une fois la base prête, elle est utilisable comme une base classique, mais en local, dans le navigateur.
Ce qu’il faut retenir sur l’usage multi-contexte d’IndexedDB
Lorsqu’une base est utilisée depuis un autre appareil ou navigateur, elle devra être recréée et éventuellement remplie à nouveau, car IndexedDB ne partage pas les données entre environnements. Il appartient donc à l’application de gérer ces ajouts, que ce soit par des données initiales ou des scripts de synchronisation. Ces opérations modifient le contenu de la base, mais pas sa structure. IndexedDB ne détecte pas ces changements comme des évolutions de version. C’est à l’application de décider si une synchronisation, une sauvegarde ou une vérification est nécessaire après modification.
Insérer des données dans le store
Une fois la base ouverte, il est possible d’y insérer les premiers enregistrements via une transaction sur le store concerné. IndexedDB permet de manipuler des objets complets, à condition d’utiliser les méthodes adaptées. Deux commandes sont disponibles pour ajouter ou modifier des données : add() et put().
Nous verrons plus en détail le fonctionnement des transactions dans l’article suivant : Explorer et modifier les données dans IndexedDB : curseurs, accès directs et transactions.
Insérer des données avec add()
La méthode add() permet d’insérer une nouvelle entrée dans le store. Chaque ligne ajoute un objet complet. Le store albums, défini précédemment avec une clé auto-incrémentée (id), génère automatiquement un identifiant unique pour chaque enregistrement.
const transaction = db.transaction('albums', 'readwrite');
const store = transaction.objectStore('albums');
store.add({ title: 'Tago Mago', artist: 'Can', year: 1971 });
store.add({ title: 'Yeti', artist: 'Amon Düül II', year: 1970 });
store.add({ title: 'Neu!', artist: 'Neu!', year: 1972 });
store.add({ title: 'Ege Bamyasi', artist: 'Can', year: 1972 });
store.add({ title: 'No. 2', artist: 'Kin Ping Meh', year: 1972 });
store.add({ title: 'Neu! 75', artist: 'Neu!', year: 1975 });Si une clé identique est déjà présente, une erreur est déclenchée. Cette méthode garantit donc qu’aucune donnée existante ne sera remplacée par inadvertance.
// Tentative d'ajout d'un doublon — déclenchera une erreur si une entrée avec id: 1 existe déjà
store.add({ id: 1, title: 'Tago Mago (Deluxe)', artist: 'Can', year: 1971 }).onerror = (event) => {
console.warn('Erreur : une entrée avec cette clé existe déjà');
};Insérer ou remplacer avec put()
La méthode put() permet d’insérer une nouvelle entrée si la clé n’existe pas encore, ou de remplacer l’enregistrement existant si elle est déjà présente. (Nous verrons cela plus en détail dans la partie consacrée à la modification des données.). Contrairement à add(), elle ne déclenche pas d’erreur en cas de doublon. Si l’objet ne comporte pas de champ de clé explicite (comme id) et que le store a été défini avec autoIncrement: true, IndexedDB générera automatiquement une clé. En revanche, si le store attend une clé explicite sans incrémentation automatique, l’absence de clé provoquera une erreur. :
// Nouvelle insertion avec put()
store.put({ id: 7, title: 'Solar Music Live', artist: 'Grobschnitt', year: 1978 });
// Insertion sans clé explicite — une clé auto-incrémentée sera générée automatiquement
store.put({ title: 'Rockpommel’s Land', artist: 'Grobschnitt', year: 1977 });
// Autre insertion sans champ id — une autre clé sera générée
store.put({ title: 'The Faust Tapes', artist: 'Faust', year: 1973 });
// Mise à jour (ou remplacement) d’une entrée déjà existante
store.put({ id: 2, title: 'Yeti (Remastered)', artist: 'Amon Düül II', year: 1970 });Ce comportement peut être utile pour injecter un jeu de données sans se soucier de l’existence préalable des entrées. Il est cependant moins strict que add(), car il peut écraser silencieusement une donnée déjà présente.
Nous approfondirons l’usage de
put()plus loin dans l’article, lorsqu’il s’agira de modifier des données existantes.
Quand utiliser add() ou put() ?
Si l’objectif est de garantir l’unicité des données lors de l’insertion, add() est la méthode recommandée. En revanche, pour injecter ou remplacer des entrées sans se soucier de doublons, put() offre plus de souplesse. Le choix entre les deux dépend donc du niveau de contrôle souhaité.
Conclusion
IndexedDB peut sembler plus complexe à aborder que localStorage ou sessionStorage, mais cette richesse ouvre la voie à des usages beaucoup plus puissants. Ce système repose sur un modèle orienté objet, évolue par version, et permet de structurer des données filtrables, persistantes entre les sessions.
Une fois les premiers concepts assimilés, cette base locale devient un outil fiable pour les applications modernes, notamment les applications offline ou les interfaces nécessitant un accès rapide à des données locales.
Dans l’article Explorer et modifier les données dans IndexedDB : curseurs, accès directs et transactions, nous verrons comment manipuler les enregistrements au sein de la base. Et dans l’article IndexedDB : relations, structures avancées et bonnes pratiques, nous aborderons des cas plus complexes : mise à jour ou suppression conditionnelle, travail avec des objets imbriqués, simulation de relations entre stores, synchronisation avec un serveur ou encore limites et bonnes pratiques en production.
