Optimisation des performances des applications JavaScript : en pratique
Les performances des applications JavaScript ne sont pas simplement un bonus agréable, mais un facteur critique influençant l'expérience utilisateur (UX), le taux de conversion et les coûts d'infrastructure. Un chargement lent de page ou un retard dans la réponse d'une API peut vous coûter jusqu'à 40 % de votre trafic. Dans cet article, nous allons examiner des méthodes pratiques d'optimisation du code JavaScript (dans l'environnement Node.js) qui vous aideront à rendre votre application plus rapide et plus efficace. Nous ne nous attarderons pas sur les bases théoriques de l'Event Loop, mais passerons directement aux techniques concrètes avec des exemples.
1. Mise en cache : réduire la charge sur le serveur
L'un des moyens les plus efficaces d'accélérer une application est d'éviter d'exécuter à nouveau des opérations coûteuses. La mise en cache permet de sauvegarder le résultat d'un calcul ou d'une requête à la base de données et de le restituer lors d'une requête ultérieure.
Mise en cache en mémoire avec Map
Pour les scénarios simples, où les données ne nécessitent pas de logique d'invalidation complexe, l'objet intégré Map est idéal. Il est plus rapide que Object et mieux adapté à l'ajout et à la suppression fréquents de clés.
const cache = new Map();
async function fetchUserData(userId) { if (cache.has(userId)) { console.log('Retour depuis le cache'); return cache.get(userId); }
// Simulation d'une requête coûteuse à la base de données const data = await new Promise((resolve) => { setTimeout(() => resolve({ id: userId, name: `User_${userId}` }), 1000); });
cache.set(userId, data); return data;}
// Premier appel (attendre 1 sec)console.time('first');await fetchUserData(1);console.timeEnd('first');
// Deuxième appel (instantané)console.time('second');await fetchUserData(1);console.timeEnd('second');Cache TTL (Time-To-Live)
Pour éviter que le cache ne devienne obsolète, il faut ajouter une durée de vie aux enregistrements. Implémentons un cache TTL simple.
class TTLMap { constructor(ttlMs = 60000) { this.ttl = ttlMs; this.store = new Map(); }
set(key, value) { const record = { value, expiry: Date.now() + this.ttl }; this.store.set(key, record); }
get(key) { const record = this.store.get(key); if (!record) return null; if (Date.now() > record.expiry) { this.store.delete(key); return null; } return record.value; }}
const userCache = new TTLMap(5000); // cache de 5 secondes2. Asynchronisme et optimisation de l'Event Loop
Node.js est monothread, mais sa nature asynchrone permet de traiter efficacement de nombreuses requêtes. Le principal ennemi des performances est les opérations synchrones qui bloquent l'Event Loop.
Évitez les versions synchrones des fonctions
Les modules fs, crypto et d'autres ont des équivalents synchrones (readFileSync, crypto.pbkdf2Sync). Utilisez-les uniquement lors de l'initialisation de l'application. À l'intérieur des gestionnaires de requêtes, utilisez uniquement les versions asynchrones.
// MAUVAIS : Bloque l'Event Loopconst data = fs.readFileSync('/path/to/file.txt', 'utf-8');
// BON : Asynchroneconst data = await fs.promises.readFile('/path/to/file.txt', 'utf-8');Divisez les tâches lourdes avec setImmediate
Si vous avez une boucle synchrone lourde (par exemple, le traitement d'un tableau de 10 millions d'éléments), elle bloquera le serveur pendant plusieurs secondes. Divisez la tâche en parties et utilisez setImmediate pour permettre à l'Event Loop de traiter d'autres requêtes entre les parties.
function processLargeArray(items, chunkSize = 1000) { let index = 0;
function processChunk() { const chunk = items.slice(index, index + chunkSize); // Traitement du morceau chunk.forEach(item => heavyOperation(item));
index += chunkSize; if (index < items.length) { // Continuer à la prochaine itération de l'Event Loop