frontend / javascript
Profiling de performance : DevTools et flame charts
Explication
Ce que vous allez apprendre
- Utiliser le panel Performance des DevTools pour enregistrer et lire un flame chart
- Distinguer le temps total d'une fonction (avec ses enfants) de son "self time"
- Chronométrer une portion de code avec
console.time/timeEndetperformance.mark/measure - Reconnaître un cas de "layout thrashing" dans du code de manipulation du DOM
- Éviter de corriger une lenteur sur la base d'une intuition non mesurée
Dans quel contexte ?
Un développeur passe une journée à optimiser une fonction de tri qu'il pense être la source d'une page lente, sans amélioration mesurable. En ouvrant enfin le panel Performance des DevTools et en enregistrant un flame chart, il découvre que 80% du temps est en réalité passé dans une fonction d'affichage qui lit puis écrit element.style.width à l'intérieur d'une boucle sur 200 éléments, provoquant un recalcul de mise en page à chaque itération. La fonction de tri, qu'il soupçonnait à tort, ne représentait que 2% du temps total.
1. L'intuition se trompe presque toujours
Une intuition sur "ce qui est lent" dans un programme est très souvent fausse : ce que l'on croit être le goulot d'étranglement ne l'est presque jamais en réalité. Il faut mesurer précisément avant de toucher au code.
Prérequis
Cette leçon s'appuie sur la leçon 8 sur le DOM (le coût d'un reflow) et sur la leçon 24 sur les internals V8 : mesurer avant d'optimiser est le prolongement pratique de ces deux leçons plus théoriques.
2. L'outil de référence : le flame chart
Le "flame chart" du panel Performance des DevTools visualise chaque appel de fonction sous forme de barre : sa largeur représente le temps total passé dedans, y compris les fonctions qu'elle appelle.
3. Une distinction essentielle pour bien lire ce graphique
Le "self time" isole le temps réellement passé dans une fonction précise, hors appels enfants. Une barre large avec peu d'enfants pointe généralement vers le vrai responsable d'une lenteur.
| Métrique | Ce qu'elle mesure | Utile pour |
|---|---|---|
| Temps total | Fonction + tout ce qu'elle appelle | Repérer une branche d'appel coûteuse |
| Self time | Temps réellement passé dans la fonction elle-même | Identifier le vrai coupable, pas juste un intermédiaire |
4. Deux outils de chronométrage, deux usages
console.time/timeEnd suffisent pour un chronométrage ponctuel rapide. performance.mark/measure s'intègrent directement à la timeline visuelle des DevTools, alignés avec les autres événements du navigateur.
5. Un exemple concret de cause cachée
Le "layout thrashing" montre comment un code apparemment innocent (lire puis écrire une propriété de style en boucle) peut forcer le navigateur à recalculer la mise en page à CHAQUE itération plutôt qu'une seule fois à la fin.
Piège fréquent
Alterner lecture (element.offsetHeight) et écriture (element.style.height = ...) de propriétés de style à l'intérieur d'une même boucle force un reflow synchrone à chaque itération. Séparez toujours toutes les lectures, puis toutes les écritures, pour ne déclencher qu'un seul reflow.
6. Ce que cet exemple prouve
C'est un exemple parfait de pourquoi mesurer révèle des causes que la lecture du code seul ne laisse pas deviner. Cette avant-dernière leçon du cours relie directement les leçons sur la mémoire (19) et les internals V8 (24) à des outils concrets, utilisables au quotidien.
Commandes & code
Profiling de performance : DevTools et flame charts
Mesurer avant d'optimiser, avec les mêmes outils qu'en production.
// --- console.time / timeEnd : chronometrage rapide, sans ouvrir le profiler ---
console.time("traitement");
const resultat = Array.from({ length: 1_000_000 }, (_, i) => i * 2).filter((n) => n % 3 === 0);
console.timeEnd("traitement"); // "traitement: 12.34ms"
// --- Plusieurs chronometres nommes simultanement ---
console.time("etape-1");
console.time("etape-2");
// ... travail ...
console.timeEnd("etape-1");
console.timeEnd("etape-2");
// --- Performance API : mesures precises et exploitables par le Performance panel ---
performance.mark("debut-rendu");
// ... rendu de l'UI ...
performance.mark("fin-rendu");
performance.measure("duree-rendu", "debut-rendu", "fin-rendu");
const mesures = performance.getEntriesByName("duree-rendu");
console.log(mesures[0].duration); // duree en millisecondes, haute precision
// Ces marks/measures apparaissent DIRECTEMENT dans la timeline du panel Performance de DevTools,
// alignes avec les autres evenements du navigateur (scripting, rendu, peinture).
// --- Nettoyer les entrees de performance accumulees (evite une fuite dans les longues sessions) ---
performance.clearMarks();
performance.clearMeasures();
// --- Lire le flame chart du panel Performance : notions cles ---
// - Chaque barre = un appel de fonction ; sa LARGEUR = temps total passe (self + appels enfants).
// - Une pile de barres empilees verticalement = la profondeur d'appel (call stack) a cet instant.
// - "Self time" (temps propre) = temps passe DANS la fonction, hors appels a d'autres fonctions.
// - "Total time" = self time + temps de TOUTES les fonctions appelees depuis celle-ci.
// - Une barre tres large avec peu d'enfants = souvent LE goulet d'etranglement a optimiser en priorite.
// - Une alternance rouge/jaune signale du travail de rendu (layout, paint) declenche par du JS.
// --- Provoquer volontairement un layout thrashing pour comprendre le probleme ---
function redimensionnementLent(elements) {
elements.forEach((el) => {
el.style.width = el.offsetWidth + 10 + "px"; // LECTURE puis ECRITURE, repetee N fois
// offsetWidth force un "reflow" synchrone a CHAQUE iteration si la precedente a ecrit le style
});
}
function redimensionnementOptimise(elements) {
// 1. Toutes les LECTURES d'abord (un seul reflow pour tout le batch)
const largeurs = elements.map((el) => el.offsetWidth);
// 2. Toutes les ECRITURES ensuite (un seul repaint)
elements.forEach((el, i) => {
el.style.width = largeurs[i] + 10 + "px";
});
}
// --- PerformanceObserver : detecter les "Long Tasks" (> 50ms, bloquent l'interactivite) ---
const observateur = new PerformanceObserver((liste) => {
for (const entree of liste.getEntries()) {
console.warn(`Tache longue detectee : ${entree.duration.toFixed(1)}ms`, entree);
}
});
observateur.observe({ entryTypes: ["longtask"] });
// --- Mesurer le temps d'un rendu React/Vue avec le Profiler dedie (mention) ---
// React DevTools Profiler et Vue DevTools offrent un flame chart specifique aux COMPOSANTS,
// avec le temps de rendu et de re-render de chaque composant, complementaire au panel Performance.
// --- Memoire : prendre un heap snapshot pour comparer avant/apres une action suspecte ---
// Dans DevTools > Memory > Heap snapshot :
// 1. Prendre un snapshot A
// 2. Effectuer l'action suspectee de fuir (ouvrir/fermer un modal, naviguer, etc.) plusieurs fois
// 3. Forcer un garbage collect (bouton corbeille)
// 4. Prendre un snapshot B et comparer ("Comparison" view) : les objets qui persistent malgre le GC
// et dont le nombre augmente a chaque iteration revelent une fuite memoire.
// --- User Timing API combinee a des Long Tasks pour un monitoring en PRODUCTION ---
function mesurerFonctionCritique(nom, fonction) {
return function (...args) {
performance.mark(`${nom}-debut`);
const resultat = fonction.apply(this, args);
performance.mark(`${nom}-fin`);
performance.measure(nom, `${nom}-debut`, `${nom}-fin`);
const [mesure] = performance.getEntriesByName(nom);
if (mesure.duration > 100) {
// envoyer a un service de monitoring reel (Sentry, Datadog RUM...)
console.warn(`${nom} anormalement lent : ${mesure.duration.toFixed(0)}ms`);
}
return resultat;
};
}Résumé
console.time/timeEndsuffisent pour un chronométrage rapide ;performance.mark/measures'intègrent directement au flame chart de DevTools.- Dans un flame chart, la largeur d'une barre = temps total, et le "self time" isole le coût propre d'une fonction (hors appels enfants).
- Grouper toutes les lectures de layout avant toutes les écritures évite le "layout thrashing" (reflows répétés).
PerformanceObserver({ entryTypes: ["longtask"] })détecte en production les tâches qui bloquent l'interactivité (> 50ms).
Exercices pratiques
Mission : traquer un coût caché derrière un self time faible
Objectif : Interpréter un flame chart où le JS mesuré n'explique pas la lenteur réelle, puis corriger un layout thrashing.
Contexte
Un développeur mesure mettreAJourListe(elements) avec console.time/timeEnd et obtient 180ms. En ouvrant le panel Performance des DevTools, il voit que les barres "Recalculate Style" et "Layout" représentent 90% de la largeur totale, alors que le "self time" de sa propre fonction JS n'est que de 5ms. Le code incriminé :
function mettreAJourListe(elements) {
elements.forEach((el) => {
el.style.height = el.offsetHeight + 5 + "px";
});
}