Déboguer les animations CSS saccadées avec DevTools
Les animations CSS saccadées résultent d’un dépassement du budget de rendu par image : à 60fps, le navigateur dispose d’environ 16,7ms pour produire chaque frame, et toute frame dont le rendu est trop long — en raison d’un recalcul de mise en page, d’une opération de peinture, ou d’un thread principal surchargé — fait chuter le taux de rafraîchissement et se manifeste par un saccade visible. Le remède est rarement « ajouter davantage de will-change ». Il s’agit d’un diagnostic : identifier quelle étape du pipeline de rendu a dépassé son budget, et sur quel thread. Cet article vous propose un workflow systématique en quatre panneaux dans Chrome DevTools — Rendering, Performance, Animations et Layers — pour remonter jusqu’à la cause d’une animation saccadée, avec un exemple concret avant/après que vous pouvez reproduire.
Le lecteur visé connaît déjà transform et opacity comme propriétés peu coûteuses, mais ne dispose pas d’une procédure de diagnostic. L’animation qui fonctionnait parfaitement sur un ordinateur portable et qui saccade sur un Android d’entrée de gamme est le cas typique. Vous avez besoin d’un protocole de triage, pas d’un énième conseil sur les propriétés CSS.
Points clés
- À 60fps, le navigateur dispose d’environ 16,7ms par frame pour exécuter les étapes style, layout, paint et composite ; manquer ce budget une seule fois produit une saccade visible (documentation Chrome DevTools sur les performances).
- Diagnostiquez dans l’ordre des panneaux : Rendering pour un contrôle visuel rapide, Performance pour identifier la cause de la frame lente, Animations pour isoler la keyframe problématique, Layers pour évaluer le coût mémoire lié au compositeur.
- Des barres violettes Recalculate Style ou Layout apparaissant directement sous une barre JavaScript jaune dans la piste Main du panneau Performance signalent un forced synchronous layout ; le triangle rouge renvoie à la ligne JS exacte en cause.
- Le compositeur peut animer
transformetopacitysans déclencher de layout ni de paint ; animerleft/top/width/heightforce un layout sur le thread principal à chaque frame (CSS Triggers). - Les animations pilotées par le défilement utilisant
animation-timelines’exécutent sur le compositeur pourtransform/opacity, de sorte que leurs saccades apparaissent dans la piste Frames, et non comme une tâche longue sur le thread principal.
Qu’est-ce que le jank dans une animation ?
Le jank désigne une frame abandonnée ou retardée que l’utilisateur peut percevoir. Pour maintenir 60fps, le navigateur doit terminer chaque frame en environ 16,7ms (1000ms ÷ 60). Cette fenêtre temporelle englobe le recalcul des styles, la mise en page, la peinture et la composition pour cette frame. Lorsqu’une seule frame dépasse ce délai, le navigateur rate son échéance, le taux de rafraîchissement effectif chute — à 30fps ou moins — et le mouvement paraît saccadé. Le guide de performance de rendu de Google formule le même constat : les modifications visuelles fluides doivent s’inscrire dans la fenêtre allouée par frame, et les animations sont l’endroit où ce budget est le plus visiblement dépassé, car l’œil suit un mouvement continu.
La raison pour laquelle une frame lente est perceptible alors qu’une requête réseau lente ne l’est pas : une animation est une séquence de frames que le cerveau intègre en mouvement. Une seule frame en retard brise cette intégration, et une pause en plein mouvement se lit comme un saut. C’est pourquoi « c’est globalement fluide » ne suffit pas — c’est la pire frame, et non la moyenne, qui détermine la qualité perçue.
Le pipeline de rendu, en bref
Chaque mise à jour visuelle traverse un pipeline fixe : parse → style → layout → paint → composite. Le navigateur analyse le HTML et le CSS pour construire le DOM et le CSSOM, calcule les styles applicables (style), détermine la géométrie et la position de chaque boîte (layout), rastérise les pixels en couches (paint), puis assemble ces couches en une image affichée (composite). L’explication du pipeline de rendu sur web.dev constitue la référence approfondie de référence ; la version courte présentée ici est tout ce dont vous avez besoin.
Le point central sur lequel repose tout cet article : chaque étape du pipeline s’exécute sur un thread spécifique et est visible dans un panneau DevTools spécifique. Le layout et le paint s’exécutent sur le thread principal, aux côtés de votre JavaScript. La composition s’exécute séparément. Une animation qui ne fait que composer — déplacer une couche existante, modifier son opacité — contourne presque entièrement le thread principal. Une animation qui déclenche un layout ramène du travail sur le thread principal à chaque frame, où il entre en concurrence avec tout le reste. C’est cette distinction que les panneaux ci-dessous vous permettent de visualiser.
Quels panneaux DevTools permettent de diagnostiquer le jank dans les animations ?
Discover how at OpenReplay.com.
Un diagnostic de jank méthodique s’effectue en quatre panneaux successifs : le panneau Rendering pour un contrôle visuel rapide (y a-t-il des repaints là où il ne devrait pas y en avoir ?), le panneau Performance pour enregistrer et identifier la cause de la frame lente, le panneau Animations pour isoler la keyframe ou la propriété problématique, et le panneau Layers pour évaluer si la promotion en couche de compositeur est bénéfique ou génère une pression mémoire. Consultez-les dans cet ordre : Rendering écarte ou confirme des catégories entières de problèmes en quelques secondes, Performance fournit la trace, Animations cible la propriété, et Layers vérifie le coût de votre correctif.
Panneau Rendering : le contrôle rapide
Le panneau Rendering est votre premier arrêt, car il répond visuellement et immédiatement à la question : votre animation repeint-elle alors qu’elle ne le devrait pas ? Ouvrez-le via le Command Menu (Cmd/Ctrl+Shift+P, tapez « Show Rendering ») ou via More Tools → Rendering (référence Chrome DevTools sur le rendu). Trois options sont importantes :
- Frame Rendering Stats affiche en temps réel le nombre de fps et une superposition de la mémoire GPU pendant l’exécution de l’animation. Une valeur qui chute bien en dessous de 60 pendant l’animation confirme l’existence d’un jank.
- Paint flashing met en évidence les zones que le navigateur repeint en les faisant clignoter en vert. Un élément qui n’anime que
transformne devrait produire aucun flash vert pendant son déplacement ; un flash vert qui suit l’animation indique que vous déclenchez un repaint. - Layer borders délimite les couches du compositeur en orange. Utilisez cette option pour confirmer qu’un élément censé être accéléré matériellement dispose bien de sa propre couche — et pour repérer des couches non intentionnelles.
Étapes :
- Ouvrez le panneau Rendering.
- Activez Paint flashing et déclenchez l’animation.
- Si l’élément animé clignote en vert pendant son déplacement, l’animation repeint à chaque frame — une propriété de layout ou de paint est en cours d’animation. Cela confirme un problème au niveau de la propriété et vous indique d’ouvrir ensuite le panneau Performance.
- Si aucun flash n’apparaît mais que le mouvement reste saccadé, le goulot d’étranglement est probablement du JavaScript sur le thread principal, et non un repaint — c’est également une question pour le panneau Performance.
Panneau Performance : identifier la cause de la frame lente
Le panneau Performance est l’endroit où vous enregistrez l’animation et lisez précisément quelle étape du pipeline a dépassé le budget de frame. Il affiche le graphique FPS, le timing par frame dans la piste Frames, l’activité du thread principal, et — dans les versions récentes de Chrome — une barre latérale Insights qui signale automatiquement les problèmes tels que les reflows forcés (référence Chrome DevTools sur les performances).
Avant d’enregistrer, limitez le CPU pour simuler les conditions de l’appareil sur lequel le jank se manifeste réellement. La documentation Chrome DevTools décrit les préréglages de limitation CPU dans les Capture settings ; le panneau propose un préréglage « 4x slowdown », l’approche recommandée étant de tester avec un ralentissement qui approxime le matériel moins puissant (référence sur les performances, limitation CPU). Cette limitation est importante car la raison la plus fréquente pour laquelle une animation CSS passe le profilage local mais saccade en production est le contexte de l’appareil : un Android d’entrée de gamme faisant tourner Chrome avec plusieurs onglets ouverts dispose d’une fraction du budget CPU d’un ordinateur de développement, que la limitation approxime sans pouvoir pleinement reproduire la pression mémoire et la charge GPU concurrente.
Étapes :
- Ouvrez le panneau Performance et cochez Screenshots.
- Dans les Capture settings (icône d’engrenage), définissez la limitation CPU sur un préréglage de ralentissement.
- Cliquez sur Record, exécutez l’animation quelques secondes, puis cliquez sur Stop.
- Lisez d’abord la piste FPS/Frames. Les marques rouges au-dessus des frames signalent celles qui ont dépassé le budget.
- Zoomez sur une frame problématique et examinez la piste Main.
Voici l’heuristique la plus utile pour déboguer les animations :
Des barres violettes sous du jaune dans la piste Main = forced synchronous layout. Le triangle rouge est votre lien direct vers le correctif.
Dans la piste du thread principal, des barres violettes Recalculate Style ou Layout apparaissant directement sous une barre JavaScript jaune signalent un forced synchronous layout — le navigateur a été contraint de résoudre la géométrie en plein milieu d’un script, parce que le JavaScript a lu une propriété de layout immédiatement après avoir modifié le DOM. Lire offsetWidth, offsetTop, ou appeler getBoundingClientRect() après une modification de style force le navigateur à vider le layout de manière synchrone ; la liste canonique de Paul Irish sur ce qui force un layout/reflow répertorie ces déclencheurs. Le triangle rouge sur la barre violette ouvre une entrée Summary avec un avertissement « Layout Forced » et un lien vers le fichier source pointant vers la ligne JS exacte. Le guide sur le layout thrashing de web.dev traite en profondeur du schéma lecture-après-écriture.
Lorsqu’il n’y a pas de violet sous le jaune, le JavaScript a terminé son travail et laissé le navigateur effectuer le rendu selon son propre calendrier. C’est la trace que vous cherchez à obtenir.
Panneau Animations : isoler la keyframe
Le panneau Animations vous permet d’inspecter, de scrubber et de ralentir les animations actives afin de localiser le jank sur une keyframe ou une propriété spécifique plutôt que sur l’animation dans son ensemble. Ouvrez-le via More Tools → Animations (documentation Chrome DevTools sur les animations). Chrome détecte les animations et les liste au fur et à mesure qu’elles se déclenchent, vous permettant d’inspecter l’animation capturée, de parcourir sa timeline et d’examiner ses keyframes.
Sa puissance de diagnostic provient de sa combinaison avec Paint flashing. Ralentir une animation à 10% de sa vitesse de lecture tout en observant Paint flashing dans le panneau Rendering est le moyen le plus rapide d’identifier quelle keyframe spécifique déclenche un repaint — le flash vert apparaît au moment précis où la valeur de la propriété problématique entre en vigueur.
Étapes :
- Ouvrez le panneau Animations et déclenchez l’animation pour qu’elle apparaisse dans la liste.
- Réglez la vitesse de lecture sur 10% (les contrôles se trouvent en haut du panneau).
- Avec Paint flashing activé, parcourez la timeline et guettez le flash vert.
- Si le flash vert apparaît à un point précis de la timeline, concentrez votre investigation sur la keyframe active à ce moment-là.
Firefox et d’autres navigateurs disposent de leurs propres inspecteurs d’animations ; Chrome est l’hypothèse de travail ici.
Panneau Layers : évaluer le coût du compositeur
Le panneau Layers indique quels éléments ont été promus vers leur propre couche de compositeur, pour quelle raison, et à quel coût mémoire — c’est ainsi que vous éviterez de saupoudrer will-change partout. Ouvrez-le via More Tools → Layers (documentation Chrome DevTools sur les couches). La sélection d’une couche révèle sa consommation mémoire et la raison de sa composition dans le panneau de détails.
La promotion est un compromis. Déplacer un élément vers sa propre couche permet au compositeur de l’animer sans repeindre les éléments voisins, mais chaque couche alloue de la mémoire GPU pour sa texture. La documentation MDN sur will-change précise explicitement que cette propriété est un dernier recours : l’appliquer à trop d’éléments gaspille des ressources, car le navigateur optimise déjà de lui-même les propriétés peu coûteuses, et une sur-promotion peut dégrader les performances. Utilisez le panneau Layers pour compter les couches promues et vérifier que chacune justifie son coût mémoire.
Exemple concret avant/après : animer left vs. transform
Animer left déclenche un layout à chaque frame ; animer transform: translateX() ne déclenche ni layout ni paint. Le même mouvement s’exécute sur un thread différent. Voici la version défaillante, qui anime left :
/* Défaillant : anime une propriété de layout */
.box {
position: absolute;
left: 0;
width: 100px;
height: 100px;
background: tomato;
animation: slide 1s ease-in-out infinite alternate;
}
@keyframes slide {
to {
left: 200px;
}
}
Ce que chaque panneau révèle avec cette version : le panneau Rendering fait clignoter la boîte en vert tout au long de l’animation, car modifier left force un layout, et le layout est toujours suivi d’un paint. La piste Main du panneau Performance se remplit de barres violettes Recalculate Style et Layout à chaque frame, et la piste Frames affiche des frames hors budget dès que vous activez la limitation CPU. left, top, width et height déclenchent tous un layout — consultez CSS Triggers pour le détail par propriété — et le layout s’exécute sur le thread principal, où il entre en concurrence avec tout le reste pour le budget de 16,7ms.
La réécriture exprime le même mouvement en utilisant uniquement transform :
/* Corrigé : anime une propriété composite uniquement */
.box {
position: absolute;
left: 0;
width: 100px;
height: 100px;
background: tomato;
animation: slide 1s ease-in-out infinite alternate;
}
@keyframes slide {
to {
transform: translateX(200px);
}
}
translateX reproduit le changement de position via transform. Après la réécriture : Paint flashing n’affiche plus de vert pendant le mouvement, la piste Main du panneau Performance ne se remplit plus de violet à chaque frame, et l’animation s’exécute sur le compositeur. Le compositeur peut animer transform et opacity sans déclencher de layout ni de paint, de sorte que le navigateur déplace une texture de couche existante au lieu de recalculer la géométrie à chaque frame.
La liste des correctifs
Le correctif consiste à remplacer les propriétés : substituez tout ce qui déclenche un layout ou un paint par transform ou opacity. Le tableau associe chaque intention d’animation à son équivalent composite uniquement.
| Intention | À éviter (force layout/paint) | À utiliser (composite uniquement) |
|---|---|---|
| Déplacer | left, top, margin | transform: translate() |
| Redimensionner | width, height | transform: scale() |
| Faire pivoter | Hacks affectant le layout | transform: rotate() |
| Fondu | Bascules visibility, changements de background | opacity |
Les propriétés CSS les plus sûres et les plus largement supportées pour les animations sont transform (translation, mise à l’échelle, rotation, inclinaison) et opacity, car les navigateurs peuvent généralement les exécuter sur le compositeur sans déclencher de layout ni de paint. filter peut également être accéléré par le GPU pour des fonctions comme blur(), mais la prise en charge et le comportement varient ; vérifiez donc dans le panneau Rendering avec Paint flashing avant de supposer que c’est gratuit — la documentation MDN sur filter décrit la propriété, et CSS Triggers enregistre son impact sur le rendu par moteur. De nombreuses autres propriétés animées déclenchent un paint, et les propriétés qui modifient la taille ou la position déclenchent généralement un recalcul de layout sur le thread principal.
Pour les animations pilotées par JavaScript, regroupez toutes les lectures DOM avant toutes les écritures DOM. Le forced synchronous layout dans la trace de l’exemple provient de la lecture d’une propriété de layout après une écriture ; regrouper les lectures en premier permet au navigateur de les servir depuis le layout de la frame précédente plutôt que d’en vider un nouveau. Le guide sur le layout thrashing détaille ce schéma.
Utilisez will-change de manière stratégique, et non par défaut. Appliquez-le à un élément que vous êtes sur le point d’animer, et supprimez-le à la fin de l’animation ; selon MDN, l’appliquer de manière générale gaspille de la mémoire GPU, car le navigateur optimise déjà de lui-même les propriétés peu coûteuses. Confirmez l’effet dans le panneau Layers.
Animations pilotées par le défilement : une signature de jank différente
Les animations pilotées par le défilement déclarées avec animation-timeline: scroll() ou animation-timeline: view() changent l’endroit où vous devez regarder dans DevTools. Lorsqu’elles n’animent que transform ou opacity, elles s’exécutent sur le compositeur, de sorte que leur jank n’apparaît pas comme une tâche longue dans la piste Main — regardez plutôt les frames abandonnées dans la piste Frames. La documentation MDN sur animation-timeline et le guide Chrome sur les animations pilotées par le défilement couvrent la fonctionnalité et sa compatibilité navigateur. Si vous appliquez l’heuristique de la piste Main et ne trouvez rien, mais que la piste Frames affiche toujours des frames hors budget, suspectez qu’une propriété non compositable s’est glissée dans les keyframes de l’animation pilotée par le défilement.
Pourquoi une animation saccade-t-elle uniquement en production ?
DevTools effectue ses profilages dans des conditions contrôlées ; la variable qu’il ne peut pas reproduire fidèlement est le contexte réel de l’utilisateur — niveau du CPU de l’appareil, pression mémoire, activité concurrente. Lorsqu’un rapport de jank ne se reproduit pas en local, ce contexte manquant en est généralement la cause. La session replay le capture, vous permettant ainsi de savoir quelles conditions simuler avant d’enregistrer.
Exécutez les quatre panneaux dans l’ordre — Rendering pour confirmer, Performance pour identifier la cause, Animations pour isoler, Layers pour évaluer — et la prochaine animation saccadée cessera d’être une affaire de conjecture.
FAQ
C'est une question de contexte de l'appareil, pas de code. Un Android d'entrée de gamme avec plusieurs onglets ouverts dispose d'une fraction du budget CPU d'un ordinateur portable. Activez la limitation CPU dans les Capture settings du panneau Performance, et utilisez la session replay pour capturer les conditions réelles lors desquelles le jank s'est produit en production.
`transform` s'exécute sur le thread du compositeur sans déclencher de layout ni de paint — le navigateur déplace simplement une texture de couche existante à chaque frame. `left` ou `top` force un recalcul de layout sur le thread principal à chaque frame, suivi d'un paint, en concurrence avec le JavaScript pour le budget de 16,7ms.
Pas de manière fiable. Seuls `transform` et `opacity` sont garantis comme étant uniquement composites. `filter` peut être accéléré par le GPU pour des fonctions comme `blur()` dans certains moteurs, mais la prise en charge varie. Vérifiez dans le panneau Rendering avec Paint flashing : un flash vert signifie qu'un repaint est effectué à chaque frame.
`animation-timeline: scroll()` et `view()` s'exécutent sur le compositeur lorsqu'ils n'animent que `transform` ou `opacity`, ne produisant aucune tâche longue sur le thread principal. Le jank se manifeste dans la piste Frames. Si la piste Main ne montre rien mais que la piste Frames affiche des frames hors budget, une propriété non compositable s'est probablement glissée dans les keyframes.
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.