Comprendre les différences entre localStorage, sessionStorage et cookies pour choisir la bonne solution de stockage côté client
Introduction aux solutions de stockage côté client
Les applications web modernes ont souvent besoin de stocker des données côté client pour améliorer l'expérience utilisateur, réduire les appels serveur et permettre un fonctionnement hors ligne partiel. JavaScript propose trois solutions principales : localStorage, sessionStorage et les cookies.
Chacune de ces technologies a ses propres caractéristiques, avantages et limitations. Comprendre leurs différences est essentiel pour choisir la solution adaptée à vos besoins spécifiques.
localStorage : stockage persistant
Le localStorage est un mécanisme de stockage de type clé-valeur qui persiste même après la fermeture du navigateur. Les données restent disponibles tant qu'elles ne sont pas explicitement supprimées.
Caractéristiques principales
- Capacité : 5 à 10 MB selon les navigateurs (généralement 5 MB)
- Persistance : Les données survivent à la fermeture du navigateur
- Portée : Partagé entre tous les onglets et fenêtres du même domaine
- Format : Stockage uniquement de chaînes de caractères (strings)
- Synchrone : Les opérations bloquent le thread principal
API localStorage
L'API est simple et intuitive, avec quatre méthodes principales :
// Stocker une donnée
localStorage.setItem('username', 'JohnDoe');
localStorage.setItem('theme', 'dark');
// Récupérer une donnée
const username = localStorage.getItem('username');
console.log(username); // "JohnDoe"
// Supprimer une donnée spécifique
localStorage.removeItem('theme');
// Supprimer toutes les données
localStorage.clear();
// Accéder directement (syntaxe alternative, moins recommandée)
localStorage.username = 'JaneDoe';
const user = localStorage.username;
Stocker des objets JSON
Comme localStorage stocke uniquement des chaînes, il faut sérialiser les objets :
// Stocker un objet
const user = {
name: 'Alice',
age: 30,
preferences: { theme: 'dark', language: 'fr' }
};
localStorage.setItem('user', JSON.stringify(user));
// Récupérer l'objet
const savedUser = JSON.parse(localStorage.getItem('user'));
console.log(savedUser.name); // "Alice"
// Vérifier l'existence avant de parser
const theme = localStorage.getItem('theme');
if (theme) {
console.log('Theme saved:', theme);
}
Vérifier la capacité disponible
// Estimer la taille utilisée en localStorage
function getLocalStorageSize() {
let total = 0;
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
total += localStorage[key].length + key.length;
}
}
return (total / 1024).toFixed(2) + ' KB';
}
console.log('localStorage size:', getLocalStorageSize());
sessionStorage : stockage temporaire
Le sessionStorage fonctionne exactement comme localStorage, mais avec une durée de vie limitée à la session du navigateur. Les données sont automatiquement supprimées lorsque l'onglet est fermé.
Caractéristiques principales
- Capacité : Identique à localStorage (5 à 10 MB)
- Persistance : Données effacées à la fermeture de l'onglet
- Portée : Isolé par onglet – chaque onglet a son propre sessionStorage
- Format : Stockage uniquement de chaînes de caractères
- API : Identique à localStorage
API sessionStorage
L'API est identique à localStorage :
// Stocker une donnée
sessionStorage.setItem('currentStep', '3');
sessionStorage.setItem('formData', JSON.stringify({ email: 'test@example.com' }));
// Récupérer une donnée
const step = sessionStorage.getItem('currentStep');
console.log(step); // "3"
// Supprimer une donnée
sessionStorage.removeItem('currentStep');
// Tout supprimer
sessionStorage.clear();
Exemple pratique : formulaire multi-étapes
// Sauvegarder l'état du formulaire à chaque étape
class MultiStepForm {
constructor() {
this.storageKey = 'multistep-form-data';
this.loadData();
}
saveStep(stepNumber, data) {
const formData = this.getData() || {};
formData[`step${stepNumber}`] = data;
formData.currentStep = stepNumber;
sessionStorage.setItem(this.storageKey, JSON.stringify(formData));
}
getData() {
const data = sessionStorage.getItem(this.storageKey);
return data ? JSON.parse(data) : null;
}
loadData() {
const data = this.getData();
if (data && data.currentStep) {
console.log(`Reprendre à l'étape ${data.currentStep}`);
return data;
}
return null;
}
clearData() {
sessionStorage.removeItem(this.storageKey);
}
}
// Utilisation
const form = new MultiStepForm();
form.saveStep(1, { name: 'John', email: 'john@example.com' });
form.saveStep(2, { address: '123 Main St', city: 'Paris' });
Tableau comparatif détaillé
Voici un comparatif complet des trois solutions :
| Critère | localStorage | sessionStorage | Cookies |
|---|---|---|---|
| Capacité | 5-10 MB | 5-10 MB | 4 KB |
| Persistance | Permanente (jusqu'à suppression manuelle) | Session (onglet fermé = données effacées) | Configurable (expires, max-age) |
| Portée | Tous les onglets du même domaine | Isolé par onglet | Tous les onglets (configurable par path/domain) |
| Envoi au serveur | ❌ Non | ❌ Non | ✅ Oui (automatique dans headers) |
| API | Synchrone, simple (getItem, setItem) | Synchrone, simple (getItem, setItem) | Chaîne à parser (document.cookie) |
| Performance | Rapide, bloquant | Rapide, bloquant | Impact sur requêtes HTTP (overhead) |
| Sécurité | Vulnérable XSS | Vulnérable XSS | Options Secure, HttpOnly, SameSite |
| Support navigateurs | IE 8+, tous les modernes | IE 8+, tous les modernes | Universel |
Cas d'usage concrets
Quand utiliser localStorage ?
- Préférences utilisateur : thème sombre/clair, langue, options d'affichage
- Cache de données : données API pour réduire les appels serveur
- Panier e-commerce : conserver les articles entre les sessions
- Historique local : dernières recherches, pages visitées
- Brouillons : sauvegardes automatiques d'articles, posts, commentaires
// Exemple : système de préférences
class UserPreferences {
constructor() {
this.key = 'user-preferences';
this.defaults = {
theme: 'light',
language: 'fr',
notifications: true
};
}
get(key) {
const prefs = this.getAll();
return prefs[key] ?? this.defaults[key];
}
set(key, value) {
const prefs = this.getAll();
prefs[key] = value;
localStorage.setItem(this.key, JSON.stringify(prefs));
}
getAll() {
const data = localStorage.getItem(this.key);
return data ? JSON.parse(data) : { ...this.defaults };
}
}
const prefs = new UserPreferences();
prefs.set('theme', 'dark');
console.log(prefs.get('theme')); // "dark"
Quand utiliser sessionStorage ?
- Formulaires multi-étapes : sauvegarder la progression sans persister
- État de navigation : filtres actifs, pagination, scroll position
- Données temporaires : résultats de recherche, sélections temporaires
- One-time tokens : jetons éphémères qui ne doivent pas survivre à la session
// Exemple : gestion d'état de filtres
class FilterState {
constructor(storageKey) {
this.key = storageKey;
}
save(filters) {
sessionStorage.setItem(this.key, JSON.stringify(filters));
}
load() {
const data = sessionStorage.getItem(this.key);
return data ? JSON.parse(data) : {};
}
clear() {
sessionStorage.removeItem(this.key);
}
}
const filters = new FilterState('search-filters');
filters.save({ category: 'electronics', priceMax: 500 });
const current = filters.load();
console.log(current.category); // "electronics"
Quand utiliser les cookies ?
- Authentification : tokens JWT, session IDs (avec HttpOnly)
- Tracking : analytics, publicité (avec consentement RGPD)
- Préférences serveur : langue, région (envoyées à chaque requête)
- CSRF tokens : protection contre les attaques CSRF
// Exemple : gestion de session authentifiée
class AuthManager {
setSession(token, expiresInDays = 7) {
// Cookie HttpOnly créé côté serveur pour le token principal
// Cookie JavaScript pour le refresh token (moins sensible)
CookieManager.set('refresh_token', token, expiresInDays, {
secure: true,
sameSite: 'Strict'
});
}
isAuthenticated() {
// Vérifier si le cookie de session existe
return CookieManager.exists('session_id') ||
CookieManager.exists('refresh_token');
}
logout() {
CookieManager.delete('refresh_token');
// Le cookie HttpOnly sera supprimé par une requête serveur
}
}
Considérations de sécurité
Vulnérabilités XSS (Cross-Site Scripting)
localStorage et sessionStorage sont vulnérables aux attaques XSS. Un script malveillant injecté peut lire toutes les données :
// ⚠️ Vulnérable si un script malveillant est injecté
const sensitiveData = localStorage.getItem('user-token');
// Le script peut voler le token
// ✅ Meilleures pratiques
// 1. Ne JAMAIS stocker de tokens sensibles dans localStorage
// 2. Valider et échapper toutes les entrées utilisateur
// 3. Utiliser Content Security Policy (CSP)
Cookies sécurisés : attributs critiques
- HttpOnly : le cookie ne peut pas être lu par JavaScript (protection XSS)
- Secure : le cookie n'est envoyé que sur HTTPS
- SameSite : protection contre les attaques CSRF
Strict: cookie jamais envoyé depuis un autre siteLax: cookie envoyé uniquement sur navigation top-levelNone: cookie envoyé partout (requiert Secure)
// ✅ Cookie sécurisé (côté serveur, exemple Node.js)
res.cookie('session_id', sessionId, {
httpOnly: true, // ✅ Protection XSS
secure: true, // ✅ HTTPS uniquement
sameSite: 'strict', // ✅ Protection CSRF
maxAge: 86400000 // 24 heures
});
// ❌ Cookie non sécurisé
document.cookie = "token=abc123"; // Vulnérable XSS, HTTP, CSRF
Bonnes pratiques de sécurité
- Ne jamais stocker de mots de passe ou tokens sensibles en clair
- Chiffrer les données sensibles avant stockage (crypto-js, Web Crypto API)
- Utiliser HttpOnly cookies pour les tokens d'authentification
- Implémenter une Content Security Policy (CSP) stricte
- Valider et échapper toutes les entrées utilisateur
- Utiliser HTTPS en production (obligatoire pour Secure cookies)
- Limiter la durée de vie des données sensibles
- Nettoyer les données lors de la déconnexion
Performance et limites
Limites de stockage
Chaque solution a des limites différentes :
// Tester la limite de localStorage (varie selon les navigateurs)
function testStorageLimit() {
try {
let i = 0;
const testKey = 'storage-test';
const chunk = new Array(1024).join('x'); // 1 KB
while (true) {
localStorage.setItem(testKey + i, chunk);
i++;
}
} catch (e) {
console.log(`Limite atteinte: environ ${i} KB`);
// Nettoyer
for (let j = 0; j < i; j++) {
localStorage.removeItem('storage-test' + j);
}
}
}
// En général : 5-10 MB pour localStorage/sessionStorage
Impact sur les performances
- localStorage/sessionStorage : opérations synchrones qui bloquent le thread principal
- Éviter de stocker de gros objets
- Utiliser des workers pour les opérations lourdes
- Cookies : ajoutent un overhead à chaque requête HTTP
- Limiter le nombre et la taille des cookies
- Utiliser le paramètre
pathpour limiter l'envoi
// ⚠️ Mauvaise pratique : parsing lourd en synchrone
const bigData = JSON.parse(localStorage.getItem('huge-dataset')); // Bloque le thread
// ✅ Meilleure approche : utiliser un worker ou limiter la taille
// Ou utiliser IndexedDB pour les gros volumes
Gestion des erreurs
// Toujours encapsuler dans try/catch
function safeLocalStorage() {
return {
set(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (e) {
if (e.name === 'QuotaExceededError') {
console.error('Quota de stockage dépassé');
// Nettoyer les anciennes données
this.cleanup();
}
return false;
}
},
get(key) {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (e) {
console.error('Erreur parsing JSON:', e);
return null;
}
},
cleanup() {
// Supprimer les données les plus anciennes
const keys = Object.keys(localStorage);
keys.slice(0, Math.floor(keys.length / 2)).forEach(key => {
localStorage.removeItem(key);
});
}
};
}
const storage = safeLocalStorage();
storage.set('user', { name: 'Alice' });
Bonnes pratiques
1. Valider l'existence et le support
// Vérifier le support de localStorage
function isLocalStorageAvailable() {
try {
const test = '__storage_test__';
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
}
if (!isLocalStorageAvailable()) {
console.warn('localStorage non disponible, utilisation de fallback');
// Utiliser un polyfill ou une alternative
}
2. Utiliser des clés préfixées
// ✅ Bon : évite les conflits entre applications
localStorage.setItem('myapp:user:preferences', data);
localStorage.setItem('myapp:cache:products', data);
// ❌ Mauvais : risque de conflit
localStorage.setItem('preferences', data);
3. Implémenter l'expiration des données
const StorageWithExpiry = {
set(key, value, ttlInSeconds) {
const now = Date.now();
const item = {
value: value,
expiry: now + (ttlInSeconds * 1000)
};
localStorage.setItem(key, JSON.stringify(item));
},
get(key) {
const itemStr = localStorage.getItem(key);
if (!itemStr) return null;
const item = JSON.parse(itemStr);
const now = Date.now();
// Vérifier l'expiration
if (now > item.expiry) {
localStorage.removeItem(key);
return null;
}
return item.value;
}
};
// Utilisation
StorageWithExpiry.set('cache:products', products, 3600); // 1 heure
const products = StorageWithExpiry.get('cache:products');
4. Écouter les événements storage
// Synchroniser les données entre onglets
window.addEventListener('storage', (event) => {
if (event.key === 'user-preferences') {
console.log('Préférences mises à jour dans un autre onglet');
console.log('Ancienne valeur:', event.oldValue);
console.log('Nouvelle valeur:', event.newValue);
// Recharger les préférences
updateUI(JSON.parse(event.newValue));
}
});
// Note : l'événement 'storage' ne se déclenche PAS dans l'onglet qui modifie
5. Créer une abstraction réutilisable
class StorageAdapter {
constructor(storage = localStorage, prefix = '') {
this.storage = storage;
this.prefix = prefix;
}
key(name) {
return this.prefix ? `${this.prefix}:${name}` : name;
}
set(name, value, ttl = null) {
const key = this.key(name);
const data = ttl ? { value, expiry: Date.now() + ttl * 1000 } : value;
this.storage.setItem(key, JSON.stringify(data));
}
get(name) {
const key = this.key(name);
const item = this.storage.getItem(key);
if (!item) return null;
const data = JSON.parse(item);
if (data.expiry && Date.now() > data.expiry) {
this.remove(name);
return null;
}
return data.value ?? data;
}
remove(name) {
this.storage.removeItem(this.key(name));
}
clear() {
if (this.prefix) {
const keys = Object.keys(this.storage);
keys.filter(k => k.startsWith(this.prefix)).forEach(k => {
this.storage.removeItem(k);
});
} else {
this.storage.clear();
}
}
}
// Utilisation
const appStorage = new StorageAdapter(localStorage, 'myapp');
appStorage.set('user', { name: 'Alice' }, 3600);
const user = appStorage.get('user');
Alternatives modernes
IndexedDB pour les gros volumes
Pour des besoins de stockage plus importants (> 10 MB) ou des données structurées complexes, utilisez IndexedDB.
- Capacité : plusieurs centaines de MB (selon le disque disponible)
- API asynchrone : ne bloque pas le thread principal
- Indexation : recherches rapides sur de gros datasets
- Transactions : opérations ACID
// Exemple simple avec IndexedDB (API complexe, privilégier une lib)
const openDB = indexedDB.open('MyDatabase', 1);
openDB.onsuccess = (event) => {
const db = event.target.result;
// Utiliser la base
};
// Mieux : utiliser une bibliothèque comme Dexie.js ou idb
import { openDB } from 'idb';
const db = await openDB('MyDatabase', 1, {
upgrade(db) {
db.createObjectStore('products', { keyPath: 'id' });
}
});
await db.add('products', { id: 1, name: 'Product A' });
Comparaison rapide avec IndexedDB
| Critère | localStorage/sessionStorage | Cookies | IndexedDB |
|---|---|---|---|
| Capacité | 5-10 MB | 4 KB | Centaines de MB+ |
| API | Synchrone, simple | Texte à parser | Asynchrone, complexe |
| Performance | Rapide (petits volumes) | Overhead HTTP | Rapide (gros volumes) |
| Cas d'usage | Préférences, cache léger | Auth, communication serveur | Données massives, offline-first |
Conclusion
Le choix entre localStorage, sessionStorage et cookies dépend de vos besoins spécifiques :
- Utilisez localStorage pour les données persistantes qui ne doivent pas être envoyées au serveur (préférences, cache)
- Utilisez sessionStorage pour les données temporaires liées à la navigation dans un onglet (formulaires multi-étapes, état)
- Utilisez les cookies pour les données qui doivent être envoyées au serveur (authentification) avec les attributs de sécurité appropriés
- Passez à IndexedDB pour les gros volumes de données structurées
Avec une bonne compréhension de ces trois solutions, vous pourrez concevoir des applications web performantes, sécurisées et offrant une excellente expérience utilisateur.