Les sites animés ont la réputation d’être lents, et c’est souvent mérité. Pourtant, mouvement et bons Core Web Vitals ne s’opposent pas. La plupart des échecs viennent d’une courte liste de causes, et chacune a une solution qui préserve l’animation.
Ce site a un hero vidéo, un mur de projets en WebGL, de la typographie WebGL et des animations pilotées par le défilement partout. Voici les règles sur lesquelles il repose.
Les trois métriques, en bref
| Métrique | Mesure | Bon |
|---|---|---|
| LCP — Largest Contentful Paint | Le moment où le contenu principal apparaît | ≤ 2,5 s |
| INP — Interaction to Next Paint | La rapidité de réaction aux taps et aux clics | ≤ 200 ms |
| CLS — Cumulative Layout Shift | L’ampleur des sauts de mise en page | ≤ 0,1 |
Google les évalue au 75ᵉ percentile des vrais utilisateurs de Chrome (les données terrain de PageSpeed Insights et de la Search Console), pas sur un test Lighthouse lancé depuis votre ordinateur. Testez sur un smartphone Android milieu de gamme avec une connexion bridée : c’est plus proche de votre 75ᵉ percentile qu’un MacBook.
LCP : montrer quelque chose de réel, tôt
Ne cachez pas le hero derrière une animation d’entrée
Le tueur de LCP le plus courant sur les sites animés, c’est un hero qui démarre à opacity: 0 et apparaît en fondu après le chargement du JavaScript. Le navigateur ne peut pas le considérer comme affiché tant qu’il n’est pas visible : le LCP attend donc votre bundle, puis l’animation.
Solution : affichez l’état final du hero en HTML et CSS. Animez l’entrée des éléments secondaires, ou partez d’un état déjà visible (léger changement d’échelle ou de position, pas une opacité nulle).
Donnez une image poster et la priorité à l’élément LCP
Si le hero est une vidéo ou un canvas WebGL, le candidat LCP est généralement son image poster. Rendez-la légère, préchargée et prioritaire :
<link rel="preload" as="image" href="/hero/poster.avif" fetchpriority="high" />
<img src="/hero/poster.avif" alt="" fetchpriority="high" width="1600" height="900" />Lancez la vidéo ou la scène WebGL après l’affichage du poster, puis passez de l’un à l’autre en fondu.
Chargez les bibliothèques lourdes quand on en a besoin
Three.js, les plugins GSAP et Lottie n’ont rien à faire dans le premier bundle s’ils animent des sections situées plus bas. Importez-les dynamiquement quand la section approche de la fenêtre :
const observer = new IntersectionObserver(async ([entry]) => {
if (!entry.isIntersecting) return
observer.disconnect()
const { initScene } = await import('./scene')
initScene(canvas)
}, { rootMargin: '400px 0px' })
observer.observe(section)Dans Next.js, next/dynamic avec ssr: false fait la même chose pour des composants entiers.
Servez des médias à la bonne taille
Une boucle vidéo en 1440p sur un téléphone, c’est de la bande passante gaspillée qui concurrence votre image LCP. Choisissez les sources selon la largeur d’écran et la connexion, et gardez les boucles du hero courtes (6 à 10 secondes, sans piste audio).
INP : garder le thread principal libre
L’INP mesure le délai entre une interaction et l’image suivante. L’animation le dégrade quand le JavaScript est occupé à chaque image.
N’animez que transform et opacity
Ces propriétés sont gérées par le compositeur et ne déclenchent pas de calcul de mise en page. Animer top, left, width, height ou margin force un recalcul à chaque image :
/* Slow: layout every frame */
.card { transition: top 0.3s; }
/* Fast: compositor only */
.card { transition: transform 0.3s; }Ne lisez pas la mise en page dans les gestionnaires de défilement
Un getBoundingClientRect() après un changement de style force un calcul de mise en page synchrone. Dans les gestionnaires de défilement, lisez une fois par image dans requestAnimationFrame, puis écrivez :
let ticking = false
window.addEventListener('scroll', () => {
if (ticking) return
ticking = true
requestAnimationFrame(() => {
const top = section.getBoundingClientRect().top // read
progressBar.style.transform = `scaleX(${clamp(-top / range)})` // write
ticking = false
})
}, { passive: true })Mieux encore : laissez une bibliothèque comme ScrollTrigger de GSAP regrouper tout cela, ou utilisez les animations CSS pilotées par le défilement (animation-timeline: scroll()) là où elles sont prises en charge.
Arrêtez le travail invisible
Chaque boucle requestAnimationFrame de la page tourne sur le thread principal. Mettez en pause canvas, vidéos et boucles quand ils sortent de la fenêtre, et quand l’onglet est masqué :
const io = new IntersectionObserver(([e]) => (e.isIntersecting ? start() : stop()))
io.observe(canvas)
document.addEventListener('visibilitychange', () => (document.hidden ? stop() : start()))Plafonnez la résolution du canvas
Un canvas WebGL à devicePixelRatio 3 sur un téléphone calcule neuf fois plus de pixels qu’en DPR 1. Plafonnez-le :
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))Pour les effets plein écran comme le brouillard ou le bruit, un rendu à demi-résolution agrandi ensuite est souvent indiscernable.
Découpez les tâches longues
Construire une scène, analyser un gros JSON ou préparer de nombreux maillages de texte peut bloquer le thread pendant des centaines de millisecondes. Découpez le travail et rendez la main entre les morceaux (await scheduler.yield() quand c’est disponible, setTimeout(0) sinon) pour qu’un tap pendant l’initialisation obtienne une réponse rapide.
CLS : réserver de la place pour tout
Donnez des dimensions aux médias
Chaque image, vidéo, iframe et canvas a besoin d’attributs width/height ou d’un aspect-ratio en CSS, pour que le navigateur réserve l’espace avant le chargement.
Chargez les polices sans saut
Les polices de titrage font partie du caractère d’un site animé, et un changement de police tardif décale chaque ligne. Préchargez le ou les deux fichiers critiques, réduisez-les aux caractères utilisés, et utilisez font-display: swap avec une police de secours aux métriques ajustées (size-adjust, ascent-override) — next/font le génère automatiquement.
Les animations ne comptent pas, les changements de mise en page si
Le CLS ignore les animations en transform, mais pas les changements de taille ou de position via des propriétés de mise en page. Un bandeau qui pousse le contenu vers le bas après le chargement compte ; le même bandeau qui glisse en transform dans un espace réservé, non.
Respecter la réduction des animations
Certains visiteurs demandent à leur système d’exploitation de limiter les animations. Respectez ce choix : c’est une exigence d’accessibilité, et ces visiteurs profitent aussi d’une page plus légère :
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}En JavaScript, vérifiez matchMedia('(prefers-reduced-motion: reduce)') avant de lancer boucles, parallaxe et vidéos en lecture automatique.
Une méthode qui détecte les régressions
- Enregistrez une trace Performance dans les Chrome DevTools avec un bridage CPU ×4, en faisant défiler toute la page. Cherchez les tâches longues et les barres violettes de mise en page.
- Activez Rendering → Paint flashing et Layout Shift Regions pour voir ce qui est redessiné et ce qui bouge.
- Consultez chaque semaine après le lancement les données terrain de PageSpeed Insights : les scores de labo sont un repère, les données terrain sont la note.
Check-list
- Contenu du hero visible dans le HTML initial, pas en fondu depuis zéro
- Image LCP préchargée avec
fetchpriority="high" - Bibliothèques lourdes chargées à la demande
- Seuls
transformetopacityanimés - Lectures de mise en page regroupées dans
requestAnimationFrame - Boucles, canvas et vidéos hors écran mis en pause
- Ratio de pixels du canvas plafonné
- Dimensions sur tous les médias ; polices avec secours aux métriques ajustées
-
prefers-reduced-motionrespecté
Pour le volet animation, voir GSAP dans React et Next.js.

