localStorage et sessionStorage suffisent pour les petits besoins : préférences, état d'un formulaire, quelques Ko de données. Mais dès qu'il faut stocker des centaines d'enregistrements, faire des recherches, ou travailler hors connexion avec une vraie structure de données, ces deux API montrent vite leurs limites. C'est là qu'IndexedDB entre en jeu.
IndexedDB est une base de données orientée objets, intégrée nativement dans les navigateurs modernes. Elle stocke des objets JavaScript directement, sans conversion en chaîne de caractères. Sa capacité tourne autour de plusieurs dizaines voire centaines de Mo selon le navigateur et les permissions accordées par l'utilisateur — largement de quoi loger un catalogue produits ou une file de messages hors ligne.
Quels sont les concepts clés d'IndexedDB ?
IndexedDB ne fonctionne pas comme une base SQL classique. Voici les notions à connaître avant d'écrire la moindre ligne de code :
- Base de données : un conteneur lié à un domaine, identifié par un nom et un numéro de version.
- Object Store : l'équivalent d'une table. On y range des objets JS, chacun identifié par une clé.
- Transaction : toute lecture ou écriture passe par une transaction — ce mécanisme garantit que les données restent cohérentes même si une opération échoue en cours de route.
- Index : une structure annexe qui permet de chercher un enregistrement par une propriété autre que la clé principale.
- Asynchrone : aucune opération ne bloque la page. On travaille avec des événements (
onsuccess,onerror) plutôt qu'avec un retour immédiat.
En résumé : une transaction IndexedDB regroupe une ou plusieurs opérations de lecture ou d'écriture sur un object store, et garantit qu'elles réussissent ou échouent ensemble. Sans elle, rien ne garantit que les données restent dans un état cohérent si une opération échoue en cours de route.
Étape 1 — Ouvrir la base de données : que fait onupgradeneeded ?
Avant de lire ou d'écrire quoi que ce soit, il faut ouvrir la base — et la créer si elle n'existe pas encore. IndexedDB s'en charge automatiquement au premier appel. L'événement onupgradeneeded est le seul endroit où l'on peut créer ou modifier la structure des object stores : il ne se déclenche que lors de la création de la base ou d'un changement de numéro de version.
Exemple : 📋 Copier le code
// Variable globale qui contiendra la connexion à la base let db; // Ouvrir la base "maBoutique" en version 1 const requete = indexedDB.open('maBoutique', 1); // Déclenché uniquement lors de la création ou d'un changement de version requete.onupgradeneeded = (event) => { db = event.target.result; // Créer un object store "produits" avec un id auto-incrémenté const store = db.createObjectStore('produits', { keyPath: 'id', autoIncrement: true }); // Index sur le nom, pour chercher un produit sans connaître son id store.createIndex('nom', 'nom', { unique: false }); }; requete.onsuccess = (event) => { db = event.target.result; console.log('Base ouverte :', db.name); }; requete.onerror = (event) => { console.error('Erreur ouverture :', event.target.error); };
Résultat attendu :
Base ouverte : maBoutique
Ce message s'affiche dans la console du navigateur (F12 → Console). La variable db reste accessible pour les étapes suivantes — c'est elle qui porte la connexion active tout au long de ce tutoriel.
⚠ Au tout premier chargement de la page, onupgradeneeded se déclenche avant onsuccess : c'est normal, IndexedDB crée la base et l'object store à ce moment-là. Aux chargements suivants, seul onsuccess s'exécute puisque la base existe déjà.
Étape 2 — Ajouter des enregistrements : comment écrire dans un object store ?
Pour écrire dans IndexedDB, on ouvre une transaction en mode readwrite, on récupère l'object store visé, puis on appelle add(). Cette méthode échoue si la clé existe déjà — contrairement à put(), vu plus loin.
Exemple : 📋 Copier le code
// Ajouter un produit dans l'object store "produits" function ajouterProduit(produit) { const transaction = db.transaction('produits', 'readwrite'); const store = transaction.objectStore('produits'); const requete = store.add(produit); requete.onsuccess = () => { console.log('Produit ajouté, id :', requete.result); }; requete.onerror = () => { console.error('Erreur ajout :', requete.error); }; } // Utilisation ajouterProduit({ nom: 'Clavier mécanique', prix: 89.99, stock: 15 }); ajouterProduit({ nom: 'Souris sans fil', prix: 34.50, stock: 42 });
Résultat attendu :
Produit ajouté, id : 1 Produit ajouté, id : 2
Comme l'object store a été créé avec autoIncrement: true, IndexedDB attribue lui-même les id 1 et 2 — inutile de les gérer à la main.
⚠ Si vous obtenez ConstraintError, c'est que add() a été appelé deux fois avec la même clé. Utilisez put() si vous voulez remplacer un enregistrement existant sans déclencher d'erreur.
Étape 3 — Lire un enregistrement par sa clé
La lecture utilise une transaction en mode readonly, moins coûteuse que readwrite puisqu'elle ne verrouille pas l'écriture. On récupère un enregistrement précis avec get() et sa clé.
Exemple : 📋 Copier le code
// Lire le produit avec l'id 1
function lireProduit(id) {
const transaction = db.transaction('produits', 'readonly');
const store = transaction.objectStore('produits');
const requete = store.get(id);
requete.onsuccess = () => {
if (requete.result) {
console.log('Produit trouvé :', requete.result);
} else {
console.log('Aucun produit avec cet id.');
}
};
}
lireProduit(1);
Résultat attendu :
Produit trouvé : {id: 1, nom: 'Clavier mécanique', prix: 89.99, stock: 15}
L'objet retourné contient exactement les propriétés passées à add(), plus la clé id générée automatiquement.
Étape 4 — Lire tous les enregistrements avec getAll()
getAll() récupère en une seule fois l'ensemble des enregistrements d'un object store — pratique pour afficher une liste complète sans boucler sur un curseur.
Exemple : 📋 Copier le code
// Récupérer tous les produits
function tousLesProduits() {
const transaction = db.transaction('produits', 'readonly');
const store = transaction.objectStore('produits');
const requete = store.getAll();
requete.onsuccess = () => {
console.log('Tous les produits :', requete.result);
};
}
tousLesProduits();
Résultat attendu :
Tous les produits : [
{id: 1, nom: 'Clavier mécanique', prix: 89.99, stock: 15},
{id: 2, nom: 'Souris sans fil', prix: 34.50, stock: 42}
]
requete.result est un tableau JavaScript ordinaire — on peut le passer directement à .map() ou .filter() pour construire une interface.
Étape 5 — Rechercher avec un index : comment chercher sans connaître la clé ?
L'index créé sur la propriété nom à l'étape 1 permet de retrouver un enregistrement sans connaître son id — utile dès que la recherche porte sur autre chose que la clé primaire.
Exemple : 📋 Copier le code
// Rechercher un produit par son nom via l'index
function rechercherParNom(nomRecherche) {
const transaction = db.transaction('produits', 'readonly');
const store = transaction.objectStore('produits');
const index = store.index('nom');
const requete = index.get(nomRecherche);
requete.onsuccess = () => {
if (requete.result) {
console.log('Produit trouvé :', requete.result);
} else {
console.log('Aucun produit avec ce nom.');
}
};
}
rechercherParNom('Souris sans fil');
Résultat attendu :
Produit trouvé : {id: 2, nom: 'Souris sans fil', prix: 34.50, stock: 42}
Sans cet index, il aurait fallu parcourir tous les enregistrements avec un curseur pour trouver le bon. L'index évite ce balayage complet.
Étape 6 — Mettre à jour un enregistrement avec put()
put() remplace un enregistrement existant ou en crée un nouveau si la clé n'existe pas — un comportement qu'on appelle généralement un « upsert ». Contrairement à add(), il ne renvoie pas d'erreur si la clé est déjà prise.
Exemple : 📋 Copier le code
// Mettre à jour le stock et le prix du produit id=1 function mettreAJourProduit(produitModifie) { const transaction = db.transaction('produits', 'readwrite'); const store = transaction.objectStore('produits'); // put() remplace l'enregistrement complet, pas seulement les champs modifiés const requete = store.put(produitModifie); requete.onsuccess = () => { console.log('Produit mis à jour.'); }; } mettreAJourProduit({ id: 1, nom: 'Clavier mécanique', prix: 79.99, stock: 10 });
Résultat attendu :
Produit mis à jour.
⚠ put() écrase tout l'objet. Si vous omettez un champ existant, par exemple stock, il disparaît de l'enregistrement — relisez l'objet d'abord si vous ne voulez modifier qu'une seule propriété.
Étape 7 — Supprimer un enregistrement
La suppression suit le même schéma que l'écriture : transaction readwrite, puis delete() avec la clé de l'enregistrement à retirer.
Exemple : 📋 Copier le code
// Supprimer le produit avec l'id 2
function supprimerProduit(id) {
const transaction = db.transaction('produits', 'readwrite');
const store = transaction.objectStore('produits');
const requete = store.delete(id);
requete.onsuccess = () => {
console.log('Produit supprimé.');
};
}
supprimerProduit(2);
Résultat attendu :
Produit supprimé.
delete() ne renvoie pas d'erreur si la clé n'existe pas — la transaction réussit silencieusement. Pour vérifier qu'un enregistrement a bien disparu, relisez-le ensuite avec get().
IndexedDB ou localStorage : lequel choisir ?
En résumé : localStorage convient aux petites données simples et synchrones, IndexedDB aux volumes plus importants et aux structures complexes. Le choix dépend surtout de la taille et de la forme des données à stocker.
- localStorage : préférences, petites chaînes, données simples. Rapide à mettre en place, mais synchrone — donc bloquant si le volume grossit.
- IndexedDB : catalogues de produits, messages, formulaires complexes, applications hors ligne. Asynchrone, structuré, capable de gérer des recherches indexées.
À partir d'une cinquantaine d'enregistrements, ou dès que la structure des données se complique, IndexedDB devient le choix logique. Je le vois surtout utile pour les applications qui doivent continuer à fonctionner sans connexion — un cas que localStorage gère mal au-delà de quelques Ko.
Récapitulatif : le cycle complet en un seul script
Exemple : 📋 Copier le code
// Script complet : ouverture, écriture, puis relecture let db; const requeteOuverture = indexedDB.open('maBoutique', 1); requeteOuverture.onupgradeneeded = (event) => { db = event.target.result; const store = db.createObjectStore('produits', { keyPath: 'id', autoIncrement: true }); store.createIndex('nom', 'nom', { unique: false }); }; requeteOuverture.onsuccess = (event) => { db = event.target.result; console.log('Base ouverte :', db.name); // Ajouter deux produits const txAjout = db.transaction('produits', 'readwrite'); const storeAjout = txAjout.objectStore('produits'); storeAjout.add({ nom: 'Clavier mécanique', prix: 89.99, stock: 15 }); storeAjout.add({ nom: 'Souris sans fil', prix: 34.50, stock: 42 }); txAjout.oncomplete = () => { // Relire tous les produits une fois l'ajout terminé const txLecture = db.transaction('produits', 'readonly'); const storeLecture = txLecture.objectStore('produits'); const requeteTous = storeLecture.getAll(); requeteTous.onsuccess = () => { console.log('Tous les produits :', requeteTous.result); }; }; }; requeteOuverture.onerror = (event) => { console.error('Erreur ouverture :', event.target.error); };
Ce script rassemble l'essentiel : ouverture de la base, création de l'object store et de l'index, ajout de deux produits, puis relecture complète une fois la transaction d'écriture terminée grâce à oncomplete. Les fonctions détaillées plus haut (lireProduit, rechercherParNom, mettreAJourProduit, supprimerProduit) restent réutilisables telles quelles, tant que db a été initialisée par ce script.
Par carabde : 17 avril 2026 | Mis à jour le 17 août 2026