Communitygithub.com

Animation Web Performante (60fps)

Élimine les saccades dans les animations CSS en remplaçant les propriétés coûteuses (layout/paint) par des équivalents basés sur transform et opacity, pour des animations 60 ou 120 fps.

Qu'est-ce que Animation Web Performante (60fps) ?

Cette compétence résout le problème le plus fréquent des animations web saccadées : l'animation de propriétés qui forcent le navigateur à recalculer la disposition (reflow) ou à repeindre chaque image. Le rendu du navigateur suit les étapes **layout → paint → composite**. Animer `width`, `height`, `top`, `left`, `margin`, `padding`, `box-shadow` ou `filter` relance le layout et/ou la peinture à chaque image, bloquant le thread principal. En revanche, animer **`transform`** et **`opacity`** s'exécute entièrement sur le compositeur (souvent le GPU), en sautant le layout et le paint : c'est ce qui permet une animation fluide à 60 ou 120 fps. La compétence fournit des techniques immédiatement applicables : utiliser `transform: translate()` et `scale()` à la place de `top`/`left`/`width`/`height`, la technique FLIP pour les changements de layout, l'animation des ombres via l'opacité d'un pseudo-élément, et l'animation de `height: auto` grâce à `interpolate-size: allow-keywords` ou la grille CSS `0fr→1fr`. Elle inclut aussi des bonnes pratiques pour éviter les “layout thrashing” et utiliser `will-change` avec parcimonie. Une section de vérification détaillée (DevTools Performance, captures d'écran headless) permet de confirmer que l’animation reste sur le compositeur. Elle s'applique à tout projet web utilisant CSS, JavaScript et le Web Animations API, avec un livrable sous forme de fichier HTML autonome.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/yokyznt/claude-skills-web-definitiva/tree/HEAD/skills/60fps-animation

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Animation Web Performante

Élimine la cause n°1 des animations web saccadées : l'animation de propriétés qui forcent le navigateur à recalculer la disposition (reflow) ou à repeindre chaque image. Le navigateur effectue le rendu en étapes — layout → paint → composite. Animer width, height, top, left, margin, padding, box-shadow ou filter relance le layout et/ou la peinture à chaque image, bloquant le thread principal. En revanche, animer transform et opacity s'exécute entièrement sur le compositeur (souvent le GPU), en sautant le layout et le paint, ce qui rend l'animation fluide à 60/120 fps.

Quand l'utiliser

À utiliser lorsqu'une animation saccade ou perd des images, qu'un effet de survol/défilement semble lourd, que vous animez une taille/position/ombre, qu'un changement de disposition nécessite une animation fluide (réorganisation de cartes, déplacement d'un élément entre conteneurs), que vous animez height: auto, ou que vous examinez du code d'animation pour en vérifier les performances.

Règle de base : n'animer que transform et opacity

Faites correspondre chaque animation « coûteuse » à un équivalent économique.

Animation (coûteuse)DéclencheRemplacer par (économique)
width / heightLayout + Painttransform: scaleX()/scaleY() (+ FLIP pour la vraie taille)
top / left / marginLayout + Painttransform: translate()
box-shadowPaintAnimer opacity d'un pseudo-élément portant l'ombre
filter: blur()Paint (lourd)Transition entre deux couches via opacity, ou accepter avec modération
background-positionPainttransform: translate() sur une couche enfant
color / backgroundPaintSouvent acceptable ; ou transition entre couches

Position et taille via transform

/* MAUVAIS : anime le layout à chaque image */
.box { transition: left 300ms, width 300ms; left: 0; width: 100px; }
.box:hover { left: 200px; width: 200px; }

/* BON : compositeur uniquement */
.box {
  transition: transform 300ms ease;
  transform: translateX(0) scaleX(1);
  transform-origin: left center;
}
.box:hover { transform: translateX(200px) scaleX(2); }

scaleX déforme le contenu interne (le texte s'étire). Pour un redimensionnement sans déformation, utilisez FLIP.

Ombre portée économique via l'opacité d'un pseudo-élément

Animer box-shadow repeint une grande zone floutée à chaque image. À la place, peignez l'ombre une fois sur un ::after, puis animez seulement son opacity.

.card { position: relative; }
.card::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: 0 12px 28px rgba(0,0,0,0.35);
  opacity: 0;
  transition: opacity 300ms ease;
  pointer-events: none;
}
.card:hover::after { opacity: 1; }

L'ombre floutée est pixellisée une fois ; le survol ne modifie qu'une opacity sur le compositeur — fluide à n'importe quelle fréquence.

FLIP : animer les changements de disposition à moindre coût

FLIP (First, Last, Invert, Play) anime un changement de disposition (réorganisation, redimensionnement, déplacement entre conteneurs) en utilisant uniquement transform. Mesurez où se trouve l'élément (First) et où il sera (Last), appliquez une transformation inverse pour qu'il reste visuellement immobile, puis animez la transform vers l'identité (Play). Le DOM se retrouve dans sa disposition finale réelle ; le mouvement est un pur travail du compositeur.

function flip(el, mutate) {
  const first = el.getBoundingClientRect();   // First
  mutate();                                    // modifier le DOM/disposition
  const last = el.getBoundingClientRect();     // Last

  const dx = first.left - last.left;
  const dy = first.top - last.top;
  const sx = first.width  / last.width;
  const sy = first.height / last.height;

  el.animate(
    [
      { transformOrigin: 'top left',
        transform: `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})` }, // Invert
      { transformOrigin: 'top left', transform: 'none' },               // Play
    ],
    { duration: 300, easing: 'cubic-bezier(0.2, 0, 0, 1)' }
  );
}

// utilisation : animer un élément se déplaçant vers une nouvelle position de grille
flip(card, () => targetContainer.appendChild(card));

L'API Web Animations animate() exécute la transformation sur le compositeur. Pour plusieurs éléments, regroupez tous les appels getBoundingClientRect() en lecture avant toute mutation (voir le layout thrashing ci-dessous).

Animer height: auto

height: auto n'est historiquement pas interpolable. Approches modernes et de repli :

/* Moderne (Chrome 129+/navigateurs compatibles) : activer les mots-clés de taille dans l'animation */
.panel {
  interpolate-size: allow-keywords;   /* permet d'animer de/vers auto */
  height: 0;
  overflow: clip;
  transition: height 300ms ease;
}
.panel.open { height: auto; }
/* calc-size() fonctionne aussi : height: calc-size(auto, size); */
/* Repli robuste aujourd'hui : grille CSS 1fr -> 0fr */
.wrapper {
  display: grid;
  grid-template-rows: 0fr;            /* repliée */
  transition: grid-template-rows 300ms ease;
}
.wrapper.open { grid-template-rows: 1fr; }
.wrapper > .content { overflow: hidden; min-height: 0; }

L'astuce de la grille anime la taille de la piste (suffisamment compatible avec le compositeur) et ne nécessite aucune mesure JS. Utilisez interpolate-size: allow-keywords / calc-size(auto, size) là où les navigateurs ciblés le supportent ; gardez la technique de grille comme défaut multi-navigateurs.

Éviter le layout thrashing (regrouper les lectures, puis les écritures)

Lire une propriété de disposition (offsetWidth, getBoundingClientRect, scrollTop, getComputedStyle) après une écriture force un reflow synchrone. Alterner lectures et écritures dans une boucle (« layout thrashing ») peut déclencher des dizaines de reflows forcés par image.

// MAUVAIS : lecture, écriture, lecture, écriture... force le reflow à chaque itération
items.forEach((el) => {
  const w = el.offsetWidth;          // lecture (force la disposition)
  el.style.width = w * 1.5 + 'px';   // écriture (invalide la disposition)
});

// BON : regrouper toutes les lectures, puis toutes les écritures
const widths = items.map((el) => el.offsetWidth);  // toutes les lectures
items.forEach((el, i) => {                          // toutes les écritures
  el.style.width = widths[i] * 1.5 + 'px';
});

Pour un travail synchronisé avec les images, lisez dans un callback requestAnimationFrame et appliquez les écritures ; des bibliothèques comme fastdom formalisent cette planification lecture/écriture.

will-change : à utiliser avec parcimonie

will-change: transform promeut un élément dans sa propre couche du compositeur à l'avance, évitant un à-coup au début de l'animation. Mais chaque couche promue coûte de la mémoire GPU, et une utilisation excessive dégrade les performances.

.menu { will-change: transform; }   /* seulement sur les éléments sur le point d'animer */

Règles : appliquez juste avant l'animation (par exemple au survol/état parent), retirez-la après (will-change: auto) lorsqu'elle est inactive, ne l'appliquez jamais de façon globale à de nombreux éléments, et ne la laissez jamais en permanence sur des nœuds volumineux/nombreux. Un simple transform: translateZ(0) fait la même promotion mais est plus difficile à annuler — préférez will-change.

Livrer et vérifier (HTML autonome)

Aide empaquetée (scripts/) : scripts/seek-shot.sh anim.html 0 1.5 3 fige le harnais ?t=N et capture chaque instant ; scripts/contact-sheet.sh sheet.png frame-*.png les assemble en une seule vue pour une vérification rapide. Voir scripts/README.md.

Le livrable est un seul .html autonome qui s'ouvre directement dans un navigateur — le balisage, l'animation CSS/JS et un harnais de gel dans un seul fichier. Pour cette compétence, la vérification est double : l'image doit être correcte et peu coûteuse à produire (compositeur uniquement).

Contrat de livraison :

  • Un .html, dépendances via CDN si nécessaire, animation via transitions CSS/@keyframes ou Web Animations API.
  • Un mécanisme de gel pour qu'une capture d'écran tombe sur une image déterministe :
    • CSS @keyframes → ?t=N définit el.style.animationDelay = (-N)+'s'; el.style.animationPlayState = 'paused'.
    • WAAPI / JS → gardez l'objet animation et anim.pause(); anim.currentTime = N*1000.

Boucle de vérification — geler → capturer, puis profiler pour les saccades :

  1. Ouvrez en mode headless au début / milieu / fin (?t=0, milieu, fin), capturez chaque image ; confirmez que le mouvement est visuellement correct (pas de texte coupé/étiré à cause de scaleX, FLIP atterrit sur la disposition réelle, les transitions d'ombre/hauteur semblent correctes).
  2. Confirmez qu'il s'agit uniquement du compositeur — tout l'intérêt de cette compétence. Soit :
    • DevTools → Performance : enregistrez l'animation, vérifiez qu'il n'y a aucune bande violette « Layout » ou verte « Paint » par image (seulement « Composite Layers »).
    • Ou trace headless : npx playwright screenshot pour le visuel, plus une capture CDP/tracing, et affirmez qu'aucun événement Layout/Paint ne se déclenche pendant la fenêtre d'animation.
  3. Vérifiez que le compteur FPS / « Frame Rendering Stats » reste à 60 — des images perdues signifient qu'une propriété coûteuse s'est glissée.
npx playwright screenshot --wait-for-timeout=500 "file://$PWD/anim.html?t=0.3" frame.png

Avant de terminer :

  1. S'ouvre de manière autonome — aucune erreur dans la console.
  2. Seules transform / opacity sont animées par image ; DevTools n'affiche aucun Layout/Paint par image.
  3. Capturé au début / milieu / fin — correct, pas de distorsion de texte due à scaleX, FLIP atterrit juste.
  4. Maintient 60fps ; will-change appliqué juste à temps et retiré lorsqu'inactif (pas de couches résiduelles).
  5. prefers-reduced-motion respecté.

Référence rapide

ObjectifAction
Déplacer un élémenttransform: translate()
Redimensionner sans déformationFLIP avec transform
Ombre au survolAnimer opacity du pseudo-élément d'ombre
Déplier à la hauteur du contenugrille 0fr→1fr, ou interpolate-size: allow-keywords
Plusieurs éléments en mouvementRegrouper les lectures getBoundingClientRect, puis les écritures
Début d'animation fluidewill-change juste à temps, retirer lorsqu'inactif
Vérifier que c'est compositeur uniquementDevTools Performance : pas de Layout violet / Paint vert par image

Pièges

  • scaleX/scaleY étire le texte et les enfants ; utilisez FLIP quand le contenu doit rester net.
  • Les pourcentages de transform sont relatifs à la boîte de l'élément lui-même, pas au parent — différent de left: %.
  • Abuser de will-change ou translateZ(0) crée trop de couches et nuit aux performances ; ne promouvez que ce qui s'anime.
  • filter et backdrop-filter sont liés au compositeur mais restent coûteux ; animez leur présence via des transitions d'opacité plutôt que le rayon de flou.
  • Le contenu de la grille 1fr→0fr doit avoir overflow: hidden et min-height: 0 sinon il ne se repliera pas.
  • Toujours conditionner les animations non essentielles derrière @media (prefers-reduced-motion: reduce).

Fichiers de référence

  • references/patterns-and-profiling.md — exemples complets exécutables (accordéon, liste réorganisable, parallaxe), procédure de profilage DevTools pour confirmer des images uniquement compositeur, motifs de mouvement réduit, et une antisèche sur le coût des propriétés.

Skills associés