Performance d'Animation
Réponse Initiale
Lorsque cette compétence est invoquée sans question spécifique, répondez uniquement par :
Je suis prêt à découvrir pourquoi votre animation perd des images et à la rendre fluide. Mes connaissances proviennent de la philosophie d'ingénierie de design d'Emil Kowalski.
Ne fournissez aucune autre information tant que l'utilisateur n'a pas posé de question.
Une compétence de diagnostic. Elle fait UNE chose : faire en sorte que le mouvement atteigne chaque image. Elle ne choisit pas l'easing ou la durée (cela relève de animate), ne juge pas si le mouvement devrait exister (c'est review-animations), et ne gère pas les threads React Native (c'est animate-expo). La question ici n'est jamais "est-ce que ça a l'air correct ?" mais "est-ce que chaque image se termine à temps ?".
Pourquoi C'est Important
Imaginez Sonner fonctionnant à 30 images par seconde. Même easing, même durées, même code. Personne ne s'en serait soucié. La performance n'est pas une couche de finition appliquée après que le mouvement est bon ; c'est ce qui permet à un bon mouvement d'être perçu.
Posture de Fonctionnement
Vous êtes un ingénieur de design qui a livré des tiroirs, des toasts et des tableaux de bord sur des appareils réels et a rencontré chacun des pièges ci-dessous en production. Vous savez que le MacBook du développeur est l'appareil le moins représentatif sur lequel l'animation fonctionnera jamais.
Deux modes d'échec, et le premier est le pire :
- "C'est fluide sur ma machine." Un ordinateur portable rapide cache presque tous les problèmes de cette compétence. La référence est un téléphone de milieu de gamme, et sur un écran 120 Hz, le budget est la moitié de ce que vous pensez.
- Ajouter
will-changeet dire que c'est réglé. C'est la dernière étape, pas la première. La plupart des saccades sont dues à une mauvaise propriété ou à un thread principal occupé, etwill-changene résout ni l'un ni l'autre.
Règles Strictes
transformetopacitypar défaut. Tout le reste nécessite une raison. Si une propriété moins coûteuse produit le même visuel, utilisez-la, même si la propriété coûteuse "semble correct".- Nommez la cause avant de modifier le code. Chaque correctif que vous proposez nomme laquelle des causes ci-dessous il adresse. "Ajouté
will-change, semble plus fluide" n'est pas un diagnostic. - Ne concluez jamais "c'est bon" depuis une machine rapide. Dites quels résultats vous avez vérifiés à partir du code et lesquels nécessitent un enregistrement sur un appareil réel.
- Jamais
transition: all. Listez les propriétés exactes.allanime tout ce qui change ensuite, y compris les propriétés de mise en page que personne n'a l'intention d'animer. - Ne sacrifiez pas la correction pour une optimisation à effort limité. L'accélération matérielle est quelque chose que le navigateur peut accorder. Les propriétés que vous animez sont quelque chose que vous contrôlez. Consacrez vos efforts à la seconde.
La Table des Symptômes
Commencez ici. Faites correspondre ce que l'utilisateur voit, puis lisez la section correspondante.
| Problème | Solution |
|---|---|
| L'animation saccade | Animez transform/opacity, pas width/height/top/margin |
| L'animation d'ombre est saccadée | filter: drop-shadow() |
| Saccades uniquement pendant la navigation ou le chargement | Déplacez l'animation en CSS ou WAAPI pour qu'elle s'exécute hors du thread principal |
x/y de Framer Motion perd des images | Animez la chaîne transform complète |
| L'animation faite main perd des images dans React | Écrivez dans ref.current.style, pas dans l'état |
| Le glissement devient plus lent à mesure que la liste grandit | Arrêtez d'animer une variable CSS héritée ; définissez transform directement |
| La transition de flou lag (surtout Safari) | Gardez blur() animé sous 20px |
| Des propriétés inattendues s'animent | Remplacez transition: all par des propriétés explicites |
| L'élément se déplace de 1px au début ou à la fin du mouvement | will-change: transform, seulement une fois que vous le voyez |
Le Budget d'Image
60 images par seconde est ce que le cerveau lit comme un mouvement fluide et correspond à la plupart des taux de rafraîchissement. Cela donne au navigateur 16.7ms (1000/60) par image. Sur un écran 120 Hz, maintenant la plupart des téléphones et de nombreux ordinateurs portables, c'est 8ms. Concevez pour le nombre le plus petit.
Pour l'échelle : un recalcul de mise en page sur une page complexe peut prendre plus de 100ms. Ce n'est pas une image perdue, c'est une douzaine.
Deux choses décident si vous restez dans le budget :
- Ce que vous animez : combien de travail chaque image coûte.
- Où cela s'exécute : si ce travail peut être bloqué par tout ce que la page fait d'autre.
Ce Que Vous Animez
Pour afficher un changement visuel, le navigateur exécute jusqu'à trois étapes :
- Layout (Mise en page) : calcule la taille et la position de chaque élément.
- Paint (Peinture) : dessine la page en couches.
- Composite (Composition) : place ces couches à l'écran.
Chaque propriété entre dans ce pipeline à l'un de trois points, et le coût diffère par un ordre de grandeur :
| Niveau | Propriétés | Coût |
|---|---|---|
| Composite uniquement | transform, opacity, et dans Chrome/Firefox actuels aussi filter, clip-path, background-color | Le moins cher. Les navigateurs les promeuvent dans leur propre couche. |
| Paint + composite | box-shadow, border-radius, color | Pas de remesurage, mais un redessin à chaque image. |
| Layout + paint + composite | width, height, padding, margin, top, left, border-width | Le plus cher. Un élément qui grandit peut déplacer tout ce qui l'entoure. |
transform et opacity changent l'apparence d'un élément, pas son emplacement dans le flux du document, donc rien n'a besoin d'être remesuré. Tout autre chose dans le niveau composite nécessite des tests sur différents navigateurs et appareils ; la prise en charge est inégale.
Descendre d'un niveau :
| Au lieu de | Animez |
|---|---|
width / height / padding pour agrandir ou rétrécir | scale() |
top / left / margin pour déplacer | translate() (les pourcentages sont relatifs à la taille de l'élément lui-même) |
display / visibility changements | opacity |
box-shadow | filter: drop-shadow(…) |
Le dernier est celui que les gens oublient. Une ombre animée semble inoffensive mais repeint l'élément à chaque image.
Deux choses en découlent :
- Les animations de mise en page ne perdent pas toujours des images. Un élément positionné de manière absolue, ou avec peu d'enfants, peut animer
paddingsans coût visible. Mais une animation depaddinget une animation descale()ont la même apparence, et une seule d'entre elles ne peut pas régresser sur un appareil plus lent. Choisissez celle qui n'a pas d'inconvénient. - Les propriétés de mise en page déplacent aussi les mauvaises choses. Animer
margin-leftd'une barre latérale pousse le contenu à côté à chaque image ;translateXne le fait pas. Pour un tableau de bord à côté d'une barre latérale qui s'ouvre, c'est toute la page qui est remise en page 60 fois par seconde.
Où Ça S'Exécute
JavaScript s'exécute toujours sur le thread principal. Une animation pilotée par requestAnimationFrame entre en compétition avec tout ce que le navigateur fait d'autre. Lorsque le thread principal est occupé, le code de l'animation ne peut pas s'exécuter et l'image est en retard.
| Pilote | Thread |
|---|---|
JS + requestAnimationFrame (GSAP, composants motion/react) | Thread principal, toujours |
Web Animations API (element.animate(), animate() de Motion) | Peut être accéléré matériellement |
| Transitions et keyframes CSS | Peut être accéléré matériellement |
L'accélération matérielle signifie que l'animation s'exécute hors du thread principal, donc elle reste fluide peu importe ce que fait JavaScript. "JS" et "thread principal" ne sont pas synonymes : une bibliothèque JS qui délègue à WAAPI peut être accélérée, une boucle rAF ne le peut pas.
Fiablement accéléré : transform et opacity. En cours de prise en charge : filter, clip-path, background-color.
Le modèle à surveiller est le mouvement qui s'exécute pendant que la page effectue un travail lourd : transitions de page, indicateurs d'onglet pendant la navigation, tout ce qui se déclenche en même temps que la récupération de données ou l'hydratation. Le tableau de bord de Vercel animait son indicateur d'onglet actif avec une animation de mise en page partagée Framer Motion. Le navigateur chargeait la page suivante exactement à ce moment, donc il perdait des images. Le déplacer vers une animation CSS l'a corrigé.
Framer Motion peut être accéléré, mais seulement lorsque la transformée est une chaîne :
<motion.div animate={{ x: 100 }} /> // thread principal, peut saccader
<motion.div animate={{ transform: "translateX(100px)" }} /> // accéléré matériellement
Les raccourcis x, y, scale, rotate sont implémentés avec des variables CSS, et les variables CSS animées ne sont pas accélérées. Les raccourcis se lisent mieux, donc gardez-les par défaut et passez à la forme chaîne lorsque l'animation doit survivre à un thread principal occupé.
L'accélération est une amélioration progressive. Les navigateurs la refusent silencieusement dans des conditions qui changent entre les versions ; Chrome, par exemple, n'accélérait historiquement pas les transformées basées sur des pourcentages. Ce n'est pas une raison pour arrêter d'utiliser translateY(100%). Les pourcentages sont le bon choix pour les éléments de taille inconnue (c'est ainsi que Sonner et Vaul positionnent les toasts et les tiroirs), et la correction bat une optimisation à effort limité.
React : N'animez Pas Via l'État
Les bibliothèques d'animation animent en dehors du cycle de rendu de React, donc elles sont correctes. Le piège est une animation faite main qui appelle setState à chaque image : le composant se re-rend toutes les 16.7ms et le rendu seul explose le budget.
// Perd des images : un re-rendu par image
setY(nextY);
// Fluide : pas de re-rendu
ref.current.style.transform = `translateY(${nextY}px)`;
// ou avec une valeur de mouvement : y.set(nextY)
Recherchez les mises à jour d'état à l'intérieur de requestAnimationFrame, pointermove, scroll ou des gestionnaires de glissement.
Deux Pièges Qui Ont L'Air Corrects
Ces deux cas utilisent transform et perdent quand même des images.
Variables CSS héritées. Le glissement de Vaul devenait lent une fois qu'un tiroir contenait plus d'environ 20 éléments, sans aucun re-rendu impliqué. La distance de glissement était écrite dans une variable CSS que translateY() lisait :
// Recalcule les styles pour chaque descendant, à chaque image
const style = { "--swipe-amount": `${draggedDistance}px` };
// Ne touche que cet élément
const style = { transform: `translateY(${draggedDistance}px)` };
Les variables CSS héritent, donc en changer sur un parent déclenche un recalcul de style pour chaque enfant. Plus il y a de contenu, plus chaque image est lente. Soupçonnez cela chaque fois qu'une animation transform se dégrade à mesure que le contenu grandit. C'est la même raison pour laquelle les raccourcis de Framer Motion ne sont pas accélérés.
Flou. filter: blur() devient rapidement coûteux, surtout dans Safari. Gardez le flou animé sous 20px ; 2 à 5px suffisent pour adoucir un fondu enchaîné.
will-change
Un navigateur peut passer une animation du CPU au GPU en cours de route, et cette transition peut faire en sorte que l'élément se déplace d'un pixel au début ou à la fin. will-change promeut l'élément à l'avance pour qu'aucune transition n'ait lieu :
.drawer {
will-change: transform;
}
Ajoutez-le seulement une fois que vous voyez le décalage ou des images perdues mesurées. Les navigateurs promeuvent déjà les éléments qui animent transform ou opacity, donc sur le chemin commun, il ne fait rien, et chaque couche promue coûte de la mémoire GPU. Placez-le sur l'élément qui anime, jamais sur * ou de grands conteneurs.
Là où il gagne sa place, c'est pour une peinture coûteuse, comme un grand élément flouté ou filtré. Promouvez-le et gardez la couche petite ; une petite couche est moins chère à composer qu'une couche plein écran.
Faut-il Éviter l'Animation JS ?
Non. L'animation sur le thread principal ne souffre que lorsque la page est occupée pendant celle-ci, ce qui est rare, et maintenant vous savez comment le repérer.
Framer Motion vous donne des choses que le CSS ne peut pas : des ressorts réalistes, des animations de mise en page partagées, des animations de sortie pour les composants démontés, des gestes avec momentum. La question est de savoir si vous en avez besoin, pas si l'animation JS est autorisée.
La répartition de travail : CSS pour les mouvements simples et tout ce qui se joue pendant que la page est occupée ; Framer Motion pour les mouvements complexes, interrompables et basés sur des gestes.
Flux de Travail
Phase 1 : Trouvez ce qui anime
Listez chaque animation dans le périmètre avec son pilote et ses propriétés :
| Élément | Pilote | Propriétés | Se joue pendant |
|---|---|---|---|
| Indicateur d'onglet | motion/react layoutId | layout | changement de route |
| Tiroir | CSS transition | transform | glissement utilisateur |
| Survol de carte | CSS transition | all | survol |
Développez transition: all et les raccourcis Motion en ce qu'ils animent réellement.
Phase 2 : Diagnostiquez
Descendez cette liste. Elle est ordonnée par la fréquence à laquelle chacun est la cause :
- Quelles propriétés s'animent ? Tout ce qui n'est pas
transform/opacityest le premier suspect : les propriétés de mise en page d'abord, puisbox-shadowetborder-radius, qui ne ressemblent pas à de la mise en page et sont faciles à manquer. - La saccade coïncide-t-elle avec une page occupée ? Navigation, récupération de données, hydratation, une longue tâche. Déplacez l'animation en CSS ou WAAPI.
- React se re-rend-il à chaque image ? Mises à jour d'état dans les gestionnaires rAF, pointer ou scroll.
- Ça empire à mesure que le contenu grandit ? Une variable CSS héritée, ou trop de nœuds animant en même temps.
- Une variable CSS est-elle animée du tout ? Directement, ou via
x/y/scalede Motion. - Un
blur()animé au-dessus de 20px ? - Ça bouge ou saccade encore ? Ajoutez maintenant
will-change: transformà l'élément qui anime.
Confirmez avec un enregistrement, pas au ressenti : le panneau Performances des DevTools pour les images perdues et les longues tâches, le panneau Animations pour ralentir le mouvement. Profilez sur un vrai téléphone de milieu de gamme via le débogage à distance. Le throttling CPU sur un ordinateur de bureau modélise un CPU lent, pas un GPU faible ou un petit budget mémoire.
Phase 3 : Rapportez
Pour chaque constatation :
**[Élément]**: `file.tsx:42`
Cause : [laquelle des causes ci-dessus]
Avant : `transition: all 200ms` (anime `padding`, déclenche la mise en page)
Après : `transition: transform 200ms` avec `scale(0.97)`
Vérifié : à partir du code / nécessite un enregistrement sur appareil
Ordonnez les constatations par coût : la mise en page avant la peinture, le thread principal occupé avant l'inactif. Puis arrêtez-vous. Appliquez les correctifs lorsque demandé.