Animations CSS sans sacrifier les performances : règles Transform/Opacity et écueils

Cet article vous a aidé ?

Tout va bien jusqu’à ce que vous mettiez en production « juste une petite animation » et que votre page se mette à glisser comme si elle traversait de la mélasse. Le pire : elle peut paraître fluide sur votre laptop, puis devenir un diaporama sur un téléphone milieu de gamme que vos clients possèdent réellement.

Les animations CSS performantes ne sont pas mystiques. C’est un problème de pipeline. Si vous comprenez ce qui déclenche le layout, le paint et le compositing — et que vous le vérifiez avec les bons outils — vous pouvez livrer du mouvement qui semble coûteux sans réellement l’être.

Le pipeline de rendu que vous payez réellement

Les navigateurs n’« animent » pas le CSS. Ils mettent à jour un graphe de scène sous des contraintes strictes : ~16,7ms par frame pour du 60Hz, ~8,3ms pour du 120Hz. Si vous manquez la date limite, l’utilisateur voit des saccades. Et les utilisateurs sont impitoyables : ils blâmeront votre produit, pas leur appareil.

Pour le travail de performance, ramenez tout à trois coûts :

  1. Layout (a.k.a. reflow) : calculer les tailles et positions des éléments. Coûteux car les changements peuvent se propager.
  2. Paint : rasteriser les pixels (texte, bordures, ombres, images) en bitmaps. Coûteux parce que les pixels sont du travail.
  3. Composite : assembler les couches peintes dans la frame finale, appliquer transforms, opacity, clipping, etc. Souvent moins cher et peut s’exécuter sur le thread du compositeur.

Quand on dit « utilisez transform et opacity », on pointe vers une vérité pragmatique : ces propriétés peuvent souvent être animées à l’étape de compositing sans relancer le layout ou le paint à chaque frame.

Ce que « uniquement au compositeur » vous apporte vraiment

Si le navigateur peut garder un élément sur sa propre couche (ou le traiter comme une surface compositée séparée), modifier transform ou opacity devient une multiplication de matrice et un mélange alpha. Ce n’est pas gratuit, mais c’est prévisible. La prévisibilité est ce qui protège votre budget par frame quand le reste de la page est occupé à… tout le reste.

Mais « uniquement au compositeur » est une promesse conditionnelle, pas une loi physique. Vous pouvez toujours déclencher des paints (ou pire, des layouts) si l’élément n’est pas isolé, s’il intersecte des effets qui nécessitent un repaint, ou si vous demandez au navigateur de faire quelque chose qui ne peut pas être différé au compositing.

Cadre SRE : la performance est un SLO

En exploitation, on n’accepte pas « généralement OK » pour la latence. La performance du mouvement mérite la même discipline. Traitez le jank comme la latence en queue : quelques mauvaises frames au 99e percentile peuvent dominer la sensation de l’UI. Vous avez besoin de :

  • conscience du budget (16,7ms est votre délai)
  • profilage (vos traces)
  • détection de régression (vos alertes)
  • garde-fous (vos limites de taux)

Et oui, vous pouvez absolument régresser la performance d’animation sans toucher à l’animation. Vous ajoutez une ombre. Vous changez une police. Vous déployez un nouvel en-tête sticky. Félicitations, votre animation compositée attend maintenant derrière du travail de paint.

La règle transform/opacity (et ce qu’elle signifie vraiment)

La règle est simple : animez transform et opacity quand la fluidité compte. La raison est moins simple : ces propriétés peuvent être appliquées au moment du compositeur en utilisant des textures pré-peintes, évitant layout et paint par frame.

Bons usages : changer comment quelque chose est dessiné, pas ce que c’est

Utilisez transform pour le mouvement et le redimensionnement, et opacity pour les fondus. Plutôt qu’animer top ou left, animez transform: translate(). Plutôt qu’animer width, animez transform: scaleX() sur un pseudo-élément ou un wrapper interne.

Mauvaises animations : changer la géométrie, le flux ou des effets lourds en peinture

Évitez d’animer des propriétés qui forcent le layout :

  • width, height
  • top, left, right, bottom (lorsqu’ils affectent le layout)
  • margin, padding
  • font-size, line-height (particulièrement pénible pour le texte)

Évitez d’animer des propriétés qui forcent des paints coûteux :

  • box-shadow (grand rayon de flou = taxe de paint)
  • filter (parfois composité, parfois brutal ; dépend du navigateur et du contexte)
  • background-position (peut nécessiter beaucoup de peinture)
  • border-radius (déclenche souvent des repaints ; peut être surprenamment coûteux à grande échelle)

« Mais j’ai besoin d’animer la hauteur » — alternatives raisonnables

Parfois vous avez vraiment besoin d’un panneau extensible. Vous avez toujours des options qui n’enflamment pas votre CPU :

  • Utilisez des transforms sur un wrapper interne : gardez le layout stable et animez un élément interne masqué avec transform: scaleY(). Associez avec transform-origin: top.
  • Utilisez max-height seulement pour de petites plages : cela déclenche toujours le layout, mais les dégâts peuvent être contenus si le sous-arbre est isolé et petit.
  • Utilisez des états discrets + reduced motion : parfois la vraie solution de performance est moins d’images.
  • Utilisez Web Animations API pour l’orchestration, mais gardez les mêmes choix de propriétés. L’API ne rend pas magiquement le layout moins coûteux.

Choisissez le bon easing et la bonne durée (oui, ça compte)

Trop court, une animation paraît glitch ; trop long, elle ressemble à un lag UI. Un bon défaut : 150–250ms pour les micro-interactions et 250–400ms pour les transitions plus larges. Si vous animez une position sur une longue distance, ajoutez un peu de temps sinon elle paraîtra téléportée.

Aussi : n’empilez pas trois courbes d’easing qui se combattent. Si votre élément scale, se déplace et s’estompe, gardez la courbe cohérente sauf raison contraire.

Blague #1 : Animer height dans un DOM volumineux, c’est comme « redémarrer la base de données » en pleine pointe : parfois ça marche, et vous ne méritez pas la victoire.

Une citation, parce que c’est vrai

Idée paraphrasée (Werner Vogels) : « Tout échoue, tout le temps. » Prévoyez-le — surtout les régressions de performance.

Faits et contexte historique utiles

Ce ne sont pas des trivia pour le plaisir. Chacun explique pourquoi le conseil « transform/opacity » existe, et pourquoi il échoue parfois.

  1. Les premières animations CSS étaient peintes comme n’importe quel changement de style. La poussée vers l’animation pilotée par le compositeur s’est accélérée avec l’adoption de rendu multithread et d’une layerisation plus agressive par les navigateurs.
  2. Les navigateurs mobiles ont forcé le sujet. Les CPU desktop pouvaient bruter beaucoup de mauvaises animations. Les limites thermiques mobiles ont rendu le jank inévitable à moins que le travail ne parte du thread principal.
  3. Les écrans à haute fréquence ont relevé la barre. Le 120Hz rend les animations médiocres plus visibles parce que vous avez la moitié du temps par frame.
  4. « Accélération GPU » n’est pas un interrupteur unique. Le compositing peut utiliser le GPU ; la rasterisation peut rester CPU ; et certains effets forcent des fallback logiciels selon les drivers et la pression mémoire.
  5. La création de couches a un coût. Promouvoir trop d’éléments en couches peut augmenter l’usage mémoire et la surcharge de compositing. Le « correctif » devient un nouveau problème.
  6. Le rendu des polices est une taxe cachée fréquente. Les animations qui font repeindre du texte — surtout avec des changements d’AA sous-pixel — peuvent ruiner la performance et paraître visuellement instables.
  7. Les éléments sticky et fixed compliquent les pipelines de scroll. Beaucoup de navigateurs optimisent le scroll en le sortant du thread principal ; certains effets (comme de lourds backdrops) peuvent le ramener.
  8. Des primitives de containment existent parce que le layout est contagieux. contain et content-visibility ont été introduits pour réduire le rayon d’impact du layout/paint dans les pages complexes.
  9. « Reduce motion » est devenu une vraie fonctionnalité plateforme pour une raison. prefers-reduced-motion n’est pas que de l’accessibilité ; c’est aussi une échappatoire performance sur les appareils lents.

Pièges : quand « GPU-accelerated » devient « GPU-annoyed »

1) Transform/opacity sont rapides… jusqu’à ce que vous forciez un repaint

Vous pouvez animer transform sur un élément qui repeint quand même parce que :

  • l’élément n’est pas sur sa propre couche et le navigateur juge le repaint moins coûteux que son isolation
  • il a des descendants lourds en peinture qui changent (par ex., des gradients animés)
  • vous le combinez avec des effets qui demandent un repaint (certains filtres, modes de blend, grandes ombres)

Règle pratique : si les pixels à l’intérieur de l’élément sont stables, le compositing gagne. Si les pixels changent, vous payez le coût du paint et le transform ne vous sauve pas.

2) will-change est un outil puissant, pas un style de vie

will-change: transform dit au navigateur « Je vais bientôt animer ceci ; préparez-vous. » La préparation signifie souvent promotion en couche et allocation de mémoire. Utile pour un petit nombre d’éléments que vous savez animer bientôt.

C’est nocif quand vous l’épandez partout « au cas où ». Modes d’échec :

  • augmentation de l’usage mémoire GPU (plus de couches, textures plus grandes)
  • plus de travail de compositing (plus de surfaces à blender)
  • plus de pression sur le cache (textures évincées et repaintées plus tard)
  • pire performance sur appareils bas de gamme (exactement où vous aviez besoin d’aide)

Utilisez will-change comme vous utilisez le préchauffage d’un cache : proche de l’événement, fortement scopié, retiré quand inutile.

3) Oscillation subpixel et la plainte « pourquoi c’est flou ? »

Les transforms se passent en espace flottant. Le texte et les lignes fines peuvent se retrouver sur des demi-pixels, déclenchant des différences d’anti-aliasing frame à frame. Vous verrez ça comme des scintillements ou un flou pendant le mouvement.

Corrections :

  • translatez en pixels entiers quand possible (arrondissez les valeurs en JS si vous pilotez les transforms)
  • évitez d’animer directement les couches de texte ; animez des conteneurs avec une rasterisation stable
  • considérez translateZ(0) avec prudence (ça peut changer le comportement de rasterisation)

4) Les grandes couches sont des couches coûteuses

Si vous promouvez un élément plein écran (ou une liste quasi plein écran) sur sa propre couche et l’animez, vous pouvez allouer des textures massives. Cela peut :

  • augmenter l’usage mémoire
  • provoquer du tiling et des repaints partiels
  • déclencher l’éviction de mémoire GPU, ce qui cause des saccades au pire moment

Un des moments les plus fréquents de « pourquoi ça s’est empiré ? » est la promotion d’un gros container scrollable parce que cela semblait une bonne idée.

5) Animations qui combattent le scroll

Le scroll est sacré. Les navigateurs mettent beaucoup d’efforts pour garder le scroll fluide, parfois en l’exécutant hors du thread principal. Si votre animation force du travail sur le thread principal pendant le scroll — layout, paint lourd, ou JS synchrone — le jank de scroll apparaît immédiatement.

Méfiez-vous particulièrement de :

  • JS piloté par le scroll qui lit le layout et écrit des styles dans le même tick
  • en-têtes sticky avec ombres/backdrops lourds
  • grandes zones avec backdrop-filter (souvent coûteuses)

Blague #2 : Votre gestionnaire de scroll n’a pas besoin d’être « temps réel ». C’est une UI, pas un desk de trading haute fréquence.

Feuille de diagnostic rapide

Voici la check-list « la page est saccadée, que faire dans les 10 prochaines minutes ? ». Elle priorise les goulets d’étranglement les plus fréquents et les étapes de désambiguïsation les plus rapides.

Premier point : confirmez quel type de jank c’est

  1. Est-ce pendant le scroll ? Si oui, soupçonnez du travail sur le thread principal (layout/paint/JS) bloquant le scroll, ou un compositing coûteux.
  2. Est-ce pendant une animation spécifique ? Si oui, soupçonnez un thrash de layout, des effets lourds en paint, trop de couches, ou des textures massives.
  3. Est-ce seulement sur certains appareils ? Si oui, soupçonnez limites mémoire GPU, différences de drivers, DPR élevé, ou throttling thermique.

Deuxième point : mesurez avant d’« optimiser »

  1. Enregistrez une trace dans DevTools Performance avec l’animation en cours.
  2. Vérifiez si des frames sont perdues à cause du Main (JS/layout) ou Raster/Paint ou Compositor.
  3. Activez le paint flashing / bordures de couches pour voir ce qui repainte et ce qui est composité.

Troisième point : appliquez la correction minimale qui supprime le goulot

  • Si le layout domine : cessez d’animer des propriétés de layout ; ajoutez du containment ; supprimez les layouts synchrones forcés en JS.
  • Si le paint domine : réduisez la zone de paint ; supprimez ombres/filters coûteux ; isolez l’élément animé ; pré-rendez les assets statiques.
  • Si le compositing domine : réduisez le nombre de couches ; évitez les surfaces promues énormes ; retirez les will-change inutiles.

Quatrième point : validez sur un profil d’appareil réaliste

Throttlez le CPU dans DevTools, testez en DPR élevé, et testez avec reduced motion. Si le correctif ne marche que sur votre machine dev, ce n’est pas un correctif ; c’est une démo.

Tâches pratiques : commandes, sorties, décisions

Vous avez demandé des tâches pratiques avec commandes, sorties et la décision à en tirer. Voici 12+ tâches qui correspondent à des workflows réels : debug local, contrôles CI, et enquêtes « pourquoi ça casse seulement en prod ? ».

Task 1: Audit des propriétés CSS animées dans votre code

cr0x@server:~$ rg -n "transition-property|transition:|@keyframes|animation:" ./src
src/components/Card.css:14:transition: box-shadow 200ms ease, transform 200ms ease;
src/components/Drawer.css:22:transition: height 250ms ease;
src/styles/animations.css:5:@keyframes pulse { from { opacity: 0.6 } to { opacity: 1 } }

Ce que ça signifie : Vous avez au moins une transition sur height et une transition sur box-shadow — risques classiques de layout/paint.

Décision : Remplacez l’animation de height par une technique basée sur transform ou isolez-la dans un petit sous-arbre. Envisagez de supprimer l’animation d’ombre ou de la simuler via l’opacité sur un élément d’ombre pré-rendu.

Task 2: Trouver l’usage de « will-change » et vérifier les abus

cr0x@server:~$ rg -n "will-change" ./src
src/styles/global.css:88:.card { will-change: transform; }
src/components/Modal.css:12:.modal { will-change: transform, opacity; }
src/components/List.css:3:.row { will-change: transform; }

Ce que ça signifie : Beaucoup d’éléments sont pré-promus, possiblement des lignes de liste (des centaines potentiellement).

Décision : Supprimez will-change de tout ce qui est répété dans de grandes listes. Appliquez-le uniquement peu avant l’interaction (hover/focus/open) et retirez-le ensuite.

Task 3: Vérifier le support reduced-motion expédié

cr0x@server:~$ rg -n "prefers-reduced-motion" ./src
src/styles/animations.css:42:@media (prefers-reduced-motion: reduce) { .anim { animation: none !important; transition: none !important; } }

Ce que ça signifie : Il y a au moins un kill switch de base.

Décision : Assurez-vous qu’il couvre les interactions lourdes (drawers, carrousels) et qu’il ne désactive pas seulement les animations décoratives tout en conservant des transitions de layout coûteuses.

Task 4: Détecter les patterns de layout-thrashing en JS (lecture/écriture imbriquée)

cr0x@server:~$ rg -n "offsetHeight|getBoundingClientRect\\(|scrollTop|clientWidth" ./src
src/pages/Home.ts:91:const h = panel.offsetHeight;
src/pages/Home.ts:92:panel.style.height = (h + 20) + "px";
src/hooks/useSticky.ts:44:const r = el.getBoundingClientRect();

Ce que ça signifie : Il y a au moins un candidat lecture-après-écriture qui peut forcer un layout synchrone à chaque frame s’il est utilisé dans une boucle ou un handler de scroll.

Décision : Groupez lectures et écritures (lisez tout d’abord, écrivez ensuite), ou passez à une approche basée sur transform qui évite de lire le layout dans le hot path.

Task 5: Produire un artifact Lighthouse JSON en CI (budget de performance de base)

cr0x@server:~$ lighthouse http://localhost:4173 --output=json --output-path=./artifacts/lh.json --quiet
...Auditing: Performance...
...Report is done...
...Saved JSON report to ./artifacts/lh.json...

Ce que ça signifie : Vous avez un artifact reproductible à comparer entre commits. Lighthouse n’« notera » pas votre animation, mais il détectera l’encombrement du main-thread et les coûts de peinture élevés qui corrèlent avec le jank.

Décision : Ajoutez des seuils (ex. total blocking time, main-thread work) comme garde-fous ; lorsqu’ils régressent, la fluidité des animations tend à régresser aussi.

Task 6: Extraire les long tasks de l’artifact Lighthouse (repérer la famine du main-thread)

cr0x@server:~$ jq '.audits["long-tasks"].details.items[:5]' ./artifacts/lh.json
[
  {
    "startTime": 1234.56,
    "duration": 245.12,
    "url": "http://localhost:4173/assets/app.js",
    "attributableToMainThread": true
  }
]

Ce que ça signifie : Les long tasks > ~50ms tuent les frames ; elles bloquent les interactions et les animations.

Décision : Scindez le travail (code splitting), différez le JS non critique, et évitez les calculs lourds pendant les transitions/scroll.

Task 7: Capturer un profil CPU en reproduisant le jank (outillage Node pour serveurs dev)

cr0x@server:~$ node --cpu-prof --cpu-prof-dir=./profiles ./node_modules/.bin/vite dev
VITE v5.0.0  ready in 312 ms
  ➜  Local:   http://localhost:5173/
...CPU profile written to ./profiles/CPU.2025-12-29T10-22-11.123Z.cpuprofile...

Ce que ça signifie : Si votre dev server sature le CPU (hot reload, transforms lourds dans les étapes de build), vous pouvez confondre le jank outil avec le jank app.

Décision : Si le serveur dev est le goulot, testez en build production. N’optimisez pas le CSS sur la base d’un artifact en mode dev.

Task 8: Construire une production build et la servir localement (supprimer le bruit dev)

cr0x@server:~$ npm run build
> build
...dist/assets/index-abc123.js  312.45 kB...
cr0x@server:~$ npx serve -s dist -l 4173
Serving!
Local: http://localhost:4173

Ce que ça signifie : Vous testez maintenant ce que les utilisateurs reçoivent : JS minifié, CSS optimisé, comportement réel du bundling.

Décision : Re-vérifiez le jank des animations en mode production avant de faire des changements. Si le problème disparaît, c’était l’outillage ou les sourcemaps, pas le CSS.

Task 9: Utiliser Playwright pour produire une trace déterministe pendant une animation

cr0x@server:~$ npx playwright test --trace on --project chromium
Running 1 test using 1 worker
  ✓ ui-animations.spec.ts:12:1 drawer open should be smooth (4.2s)
Trace file: test-results/ui-animations-drawer/trace.zip

Ce que ça signifie : Vous pouvez rejouer ce qui s’est passé et le corréler avec l’activité JS et le timing. C’est la chose la plus proche d’un « test de régression perf » qui ne vous ruine pas la vie.

Décision : Si un commit change significativement les traces (plus de long tasks pendant l’animation), bloquez la merge ou corrigez la régression avant de toucher les utilisateurs.

Task 10: Vérifier les images géantes qui transforment des fondus en pipeline bande passante→paint

cr0x@server:~$ find ./dist -type f -name "*.png" -o -name "*.jpg" -o -name "*.webp" | xargs -I{} sh -c 'printf "%8s  %s\n" "$(stat -c%s "{}")" "{}"' | sort -nr | head
 5242880  ./dist/assets/hero-background.jpg
 1310720  ./dist/assets/product-shot.png
  786432  ./dist/assets/logo.png

Ce que ça signifie : Les assets volumineux augmentent les coûts de décodage et de raster. Faire un fondu sur un hero de 5Mo peut encore saccader si le décodage/raster intervient pendant la transition.

Décision : Redimensionnez/comprimez les assets ; préchargez les images critiques ; évitez d’animer de grandes surfaces nouvellement décodées à l’écran.

Task 11: Inspecter des proxys de nombre de couches en trouvant des transforms massifs

cr0x@server:~$ rg -n "transform:" ./src | head -n 10
src/components/Row.css:7:transform: translateZ(0);
src/components/Card.css:22:transform: translateY(-2px);
src/components/Toast.css:18:transform: translateX(0);
src/components/Toast.css:22:transform: translateX(120%);

Ce que ça signifie : translateZ(0) est souvent utilisé comme hack « forcer une couche ». Utilisé massivement, il peut gonfler le nombre de couches et la mémoire.

Décision : Supprimez le translateZ(0) global. Ajoutez la promotion de couche seulement là où le profilage montre un gain.

Task 12: Repérer les effets CSS coûteux qui repaintent souvent (ombres, filtres, backdrops)

cr0x@server:~$ rg -n "box-shadow:|filter:|backdrop-filter:" ./src
src/styles/global.css:55:box-shadow: 0 20px 60px rgba(0,0,0,0.35);
src/components/Header.css:19:backdrop-filter: blur(12px);
src/components/Avatar.css:9:filter: drop-shadow(0 6px 10px rgba(0,0,0,0.25));

Ce que ça signifie : Ce sont des coupables fréquents de paint, surtout combinés à l’animation ou au scroll.

Décision : Réduisez le rayon de flou, réduisez la zone affectée, ou remplacez par des assets pré-rendus. Pour les backdrops : limitez le flou aux petites régions ou fournissez un fallback non flou pour les appareils bas de gamme.

Task 13: Détecter vite si vous animez le layout via des shorthand transitions

cr0x@server:~$ rg -n "transition:\s*all" ./src
src/components/Button.css:4:transition: all 200ms ease;
src/components/Panel.css:11:transition: all 300ms ease-in-out;

Ce que ça signifie : transition: all est un piège. Il peut commencer à animer des propriétés affectant le layout par accident quand quelqu’un change le CSS plus tard.

Décision : Remplacez par des propriétés explicites (ex. transition: transform 200ms ease, opacity 200ms ease) et faites appliquer via linting.

Task 14: Ajouter une règle stylelint rapide pour empêcher les régressions « transition: all »

cr0x@server:~$ cat .stylelintrc.json
{
  "rules": {
    "declaration-property-value-disallowed-list": {
      "transition": ["/all\\s/"]
    }
  }
}

Ce que ça signifie : Cela bloque la régression de performance la plus commune dans les transitions CSS.

Décision : Mettez-le en CI, échouez la build sur violations, et forcez des transitions explicites pour que les caractéristiques de performance restent stables dans le temps.

Trois mini-histoires d’entreprise tirées du terrain

Mini-histoire 1 : l’incident causé par une mauvaise hypothèse

Une équipe produit a déployé un tableau de bord redesigné avec une belle animation « cartes qui flottent ». L’implémentation était disciplinaire : seulement transform et opacity. Tout le monde s’est félicité, car ils avaient mémorisé la règle.

En quelques jours, le support client a commencé à recevoir des tickets : « le scroll se fige », « les boutons ne répondent pas », « la page rame ». Cela ne se reproduisait pas sur les appareils flagship. Cela se reproduisait sur des téléphones anciens et certains laptops Windows d’entreprise avec drivers GPU conservateurs.

La mauvaise hypothèse était simple : transform/opacity signifie toujours seulement compositeur. Dans ce cas, chaque carte contenait un subtil gradient animé (effet de « skeleton loading ») implémenté comme animation de background. Ces backgrounds repintaient. Donc le « cheap » transform sur 30 cartes est devenu « paint 30 cartes plus compositing » à chaque frame.

La correction n’a pas été héroïque. Ils ont supprimé le shimmer une fois les données arrivées, réduit le nombre de cartes animées simultanément, et contraint la peinture avec un wrapper utilisant le containment. Ils ont aussi ajouté un fallback reduced-motion qui désactivait entièrement le shimmer. Le dashboard a arrêté de saccader, et l’équipe a discrètement retiré « GPU-accelerated » de leur slide interne.

Mini-histoire 2 : l’optimisation qui a mal tourné

Une autre organisation avait un système de modals utilisé partout. Quelqu’un a vu que l’ouverture du modal dropait parfois des frames, alors il l’a « optimisé » en ajoutant will-change: transform, opacity au modal et à l’overlay, plus translateZ(0) à quelques composants « pour s’assurer qu’ils restent sur le GPU ».

Ça marchait isolément. Ça a aussi introduit une fuite lente de performance ailleurs. Les pages avec de longues listes se sont notablement dégradées après quelques interactions. La mémoire du process GPU augmentait et ne redescendait pas vite. Les utilisateurs décrivaient « ça devient plus lent plus je l’utilise », symptôme étrangement exact.

Le problème racine : promotion de couches partout. Les lignes de liste avec will-change sont devenues leurs propres surfaces. Overlay et modal restaient promus même quand ils n’animai(ent) pas. Le compositeur avait plus de travail et plus de pression mémoire. Sous pression, les textures étaient évincées, puis re-rasterisées plus tard — causant du jank lors d’interactions sans rapport.

Le rollback a été simple : retirer les hacks translateZ(0), limiter will-change à la fenêtre courte juste avant l’animation, et le nettoyer après la fin de l’animation. Le gain de performance est revenu, et la plainte de dégradation lente a disparu. La leçon : will-change n’est pas un assaisonnement de performance à parsemer partout.

Mini-histoire 3 : la pratique ennuyeuse mais correcte qui a sauvé la mise

Une équipe plateforme maintenait un design system utilisé par des dizaines d’équipes. Elles avaient été brûlées par des régressions d’animation, alors elles ont fait quelque chose de profondément peu sexy : codifier des primitives d’animation et interdire par défaut les transitions risquées.

Boutons, cartes, toasts et drawers utilisaient tous des mixins partagés : transforms pour le mouvement, opacity pour les fondus. Pas de transition: all. Pas d’animation de propriétés de layout dans les composants communs. Si un composant avait vraiment besoin d’une animation de hauteur, il devait être isolé derrière un pattern de wrapper et documenté.

Puis est arrivé un rebranding majeur : nouvelle typographie, ombres plus lourdes, plus de blur. L’application aurait dû devenir saccadée. Au lieu de ça, les dégâts ont été contenus parce que les animations coeur ne dépendaient pas de propriétés lourdes en paint. Quand certains écrans ont régressé, les traces étaient lisibles : on pointait des pics de paint causés par de nouveaux effets visuels, plutôt que de courir après des « bugs d’animation CSS » fantômes.

Ils ont livré à temps, et la file d’incidents est restée calme. Personne n’a écrit d’article de célébration parce que le seul changement visible était que rien n’a pris feu. C’est le boulot.

Erreurs courantes : symptômes → cause → correctif

1) Symptom : l’animation saccade seulement au démarrage

Cause : promotion de couche et rasterisation tardives (première frame de l’animation), ou décodage d’image/police en même temps.

Correctif : pré-promouvoir brièvement avec will-change juste avant l’animation ; précharger les images critiques ; éviter les swaps de polices au premier usage pendant l’animation.

2) Symptom : les effets hover paraissent « lourds » et laggent l’entrée

Cause : le hover déclenche des propriétés lourdes en paint (flou d’ombre) sur beaucoup d’éléments dans une grille ; la région de repaint est grande.

Correctif : animez plutôt transform/opacity ; simulez les ombres via l’opacité sur un pseudo-élément ; réduisez le rayon de flou ; réduisez le nombre d’éléments survolés simultanément.

3) Symptom : panneaux qui s’ouvrent/ferment perdent beaucoup de frames

Cause : animer height/max-height provoque un recalcul de layout pour un grand sous-arbre à chaque frame.

Correctif : gardez le layout stable et animez un wrapper interne avec transform: scaleY() + clipping ; ajoutez contain: layout paint quand c’est sûr ; évitez de lire le layout à chaque tick.

4) Symptom : jank de scroll après ajout d’un header sticky

Cause : élément sticky avec paint coûteux (ombre/backdrop blur) force du travail sur le main-thread pendant le scroll ; le scroll ne peut plus rester async.

Correctif : simplifiez les visuels sticky ; réduisez la zone de blur/backdrop ; envisagez un header couleur unie sur les appareils bas de gamme ; vérifiez avec le paint flashing.

5) Symptom : le texte est flou pendant l’animation

Cause : positionnement subpixel lors des transforms ; la rasterisation change pendant le mouvement.

Correctif : animez le conteneur plutôt que le texte ; arrondissez les valeurs de translate ; évitez de scaler du texte ; testez sur plusieurs navigateurs (les heuristiques de rasterisation diffèrent).

6) Symptom : la performance se dégrade dans le temps, pas immédiatement

Cause : trop de couches promues (souvent via will-change ou translateZ(0)) causant pression mémoire et churn de textures.

Correctif : retirez les promotions persistantes ; gardez le nombre de couches bas ; limitez will-change aux animations actives seulement.

7) Symptom : « l’animation transform est lente » sur un gros élément

Cause : l’élément est énorme ; le composer le composite chaque frame est coûteux ; upload de texture ou tiling peut se produire.

Correctif : réduisez la taille de la couche (animez un enfant plus petit) ; évitez les surfaces promues en plein écran ; simplifiez les visuels ; préférez des fondus par opacity pour les grandes régions.

8) Symptom : l’animation casse quand le contenu change durant la transition

Cause : mélange de changements de layout avec animation transform ; le chargement de contenu déclenche un reflow en plein vol.

Correctif : verrouillez les tailles pendant l’animation ; évitez d’insérer des nœuds DOM en plein vol ; animez des placeholders, puis swappez le contenu après.

Listes de contrôle / plan étape par étape

Checklist A : concevoir une animation qui reste rapide

  1. Définissez le rôle : décoratif ou fonctionnel ? Si fonctionnel, priorisez la réactivité sur l’ornement.
  2. Choisissez les propriétés : par défaut transform + opacity. Évitez les propriétés de layout sauf si le sous-arbre animé est minuscule.
  3. Stabilisez le paint : évitez d’animer ombres floues, filtres et grands gradients.
  4. Limitez le scope : animez un conteneur, pas des dizaines d’enfants, sauf si vous avez profilé.
  5. Choisissez les durées : 150–250ms pour petites interactions ; 250–400ms pour transitions plus larges. Plus court n’est pas toujours meilleur.
  6. Prévoyez reduced motion : désactivez ou simplifiez l’effet via prefers-reduced-motion.

Checklist B : déployer sans régressions

  1. Interdire transition: all dans les composants partagés. Des transitions explicites gardent la performance stable.
  2. Lint pour les patterns risqués : will-change massifs, translateZ(0) global, lectures de layout dans les handlers de scroll.
  3. Profiler sur un profil lent : throttle CPU et testez sur hardware milieu de gamme.
  4. Enregistrez des traces avant/après : joignez-les à la PR pour éviter les débats « ça marche sur ma machine ».
  5. Mesurez les long tasks : la performance d’animation est souvent un problème d’ordonnancement JS déguisé.

Checklist C : corriger une animation janky existante

  1. Identifiez l’étape coûteuse : main thread vs paint vs compositing.
  2. Si le main thread est chaud : supprimez les animations de layout, groupez lectures/écritures DOM, coupez les long tasks.
  3. Si le paint est chaud : simplifiez les visuels, réduisez la zone de repaint, retirez les effets flous, isolez avec containment.
  4. Si le compositeur est chaud : réduisez le nombre et la taille des couches, retirez les promotions inutiles.
  5. Validez à nouveau : même reproduction, même profil d’appareil, même méthode de mesure.

FAQ

1) Est-ce que transform et opacity sont toujours « gratuits » à animer ?

Non. Ils sont souvent moins coûteux parce qu’ils peuvent être appliqués au moment du compositeur, mais vous payez quand même le coût du compositing, et vous pouvez toujours déclencher paint/layout selon le contexte.

2) Dois-je utiliser will-change partout pour forcer des animations fluides ?

Non. Utilisez-le parcimonieusement et temporairement. Une surutilisation augmente la pression mémoire et la surcharge de compositing, ce qui peut empirer la performance — surtout sur appareils bas de gamme.

3) translateZ(0) est-il toujours une bonne astuce ?

Comme solution ciblée : parfois. Par défaut : non. C’est un instrument grossier qui peut créer trop de couches et causer une dégradation de performance à long terme.

4) Pourquoi animer box-shadow est si pénible ?

Parce que de grandes ombres floues sont lourdes en paint. Si vous les animez, vous repaintez souvent une large zone à chaque frame. Simulez-les via l’opacité sur un pseudo-élément, réduisez le flou, ou évitez de les animer.

5) Qu’en est-il d’animer filter ou backdrop-filter ?

Parfois c’est composité, parfois coûteux, et le coût varie selon le navigateur et l’appareil. Traitez les filters comme « profiler d’abord ». Pour les backdrops, réduisez agressivement la zone affectée.

6) Si je n’anime que des transforms, pourquoi je vois encore du jank ?

Causes fréquentes : long tasks JS sur le main-thread (bloquant la soumission de frames par le compositeur), promotion de couche tardive, misses de cache raster, couches énormes, ou d’autres parties de la page qui repaient pendant l’animation.

7) L’animation CSS est-elle toujours meilleure que l’animation JS ?

Non. Le CSS est excellent pour le mouvement simple et déclaratif. Le JS (ou Web Animations API) est mieux pour l’orchestration, l’interruption et le séquencement. Mais le choix de propriété compte plus que l’API.

8) Comment animer un drawer qui s’étend sans animer la hauteur ?

Utilisez un wrapper interne que vous scalez en Y avec transform: scaleY() et masquez le débordement. Gardez le layout externe stable pour que le reste de la page ne reflowe pas à chaque frame.

9) Pourquoi c’est fluide sur desktop mais pas sur mobile ?

Les appareils mobiles ont des budgets CPU et GPU plus serrés, des DPR plus élevés et du throttling thermique. Votre « petite » zone de paint peut devenir énorme en pixels réels, et la pression mémoire arrive plus vite.

10) Dois-je désactiver les animations pour tout le monde si la performance est mauvaise ?

Ne punissez pas tous les utilisateurs pour un sous-ensemble d’appareils. Fournissez prefers-reduced-motion, simplifiez les effets les plus lourds, et corrigez la cause racine. Désactivez le décoratif qui n’en vaut pas la peine.

Conclusion : prochaines étapes actionnables

Si vous voulez des animations CSS fluides en production, cessez de traiter « transform et opacity » comme une superstition et commencez à les traiter comme une hypothèse à vérifier.

  1. Auditez : supprimez transition: all, trouvez les propriétés animées de layout, et effacez les hacks de couche globaux.
  2. Profilez : enregistrez des traces et identifiez si le goulot est le layout, le paint, ou le compositing.
  3. Corrigez minimalement : passez à transform/opacity, contenez le layout quand c’est sûr, et réduisez les effets lourds en paint.
  4. Garde-fous : ajoutez des règles de lint et des artifacts CI pour que les régressions ne soient pas déployées un vendredi soir.
  5. Validez dans la réalité : profils d’appareils lents, builds production, et support reduced motion.

Vos utilisateurs se moquent que votre animation soit « techniquement accélérée GPU ». Ils veulent que l’UI réponde immédiatement et que le scroll reste fluide. Construisez pour ça.

← Précédent
Bases de l’empoisonnement du cache DNS : durcir votre résolveur sans suringénierie
Suivant →
Proxmox CIFS « Permission denied » : corriger les identifiants, le dialecte SMB et les options de montage

Laisser un commentaire