Votre API fonctionne. Votre base de données fonctionne. Votre CDN fonctionne. Et pourtant des utilisateurs se plaignent que la page « donne l’impression d’être lente ».
C’est parce que la performance perçue est une caractéristique produit, et vos rectangles blancs vides sont essentiellement un message d’erreur de chargement avec de bonnes manières.
Les écrans squelettes — surtout en pur CSS — peuvent rendre un produit réactif sans mentir. Mais mal faits, ils
consomment la batterie, provoquent des saccades sur des appareils bas de gamme, déclenchent la sensibilité au mouvement et grèvent silencieusement votre budget de rendu.
Voici le guide pratique orienté production : comment les construire, comment les garder rapides et comment les déboguer quand ils tournent mal.
Ce que sont les écrans squelettes (et ce qu’ils ne sont pas)
Un écran squelette est une interface de remplacement qui ressemble à la mise en page finale. Pas au contenu final — seulement à la structure.
L’objectif est de réduire la latence perçue et d’éviter l’effet « page blanche → saut soudain ». Il vous donne du temps pendant que
le réseau, le JS, les images et le rendu font leur travail.
Les squelettes ne remplacent pas la performance. Ils sont un contrat avec l’utilisateur : « Nous travaillons, voici où les éléments vont apparaître. »
Si votre application attend régulièrement deux secondes pour un JSON qui pourrait être mis en cache, votre squelette est essentiellement une excuse décorative.
Les meilleurs écrans squelettes réussissent trois choses :
- Stabilité : ils réservent l’espace pour éviter les sauts de page (CLS bas).
- Croyabilité : ils correspondent suffisamment à la mise en page finale pour paraître intentionnels.
- Efficacité : ils ne coûtent pas plus à animer que le contenu qu’ils masquent.
Les écrans squelettes en pur CSS sont attractifs parce qu’ils réduisent le travail JS, se déploient vite et se dégradent proprement.
Mais « pur CSS » n’est pas automatiquement synonyme de « CSS rapide ». Le CSS peut être coûteux à sa manière.
Faits intéressants et un peu d’histoire
- Fait 1 : Les écrans squelettes sont devenus courants au milieu des années 2010 quand les applications mobiles ont popularisé le « chargement conscient du contenu » plutôt que les spinners.
- Fait 2 : L’effet classic de lueur est essentiellement une surbrillance mobile sur une couleur de base — similaire aux astuces de « specular sweep » utilisées dans les anciens effets brillants d’UI.
- Fait 3 : Les navigateurs modernes rendent les pages via un pipeline (style → layout → paint → composite). L’animation des squelettes peut solliciter le paint ou le composite selon ce que vous animez.
- Fait 4 : Animer
transformetopacityest généralement moins coûteux parce que cela peut rester sur le compositor et éviter les repaints. - Fait 5 : Animer les positions de dégradé déclenche souvent des repaints ; les dégradés ne sont pas « gratuits », et un grand dégradé scintillant peut devenir un impôt par frame.
- Fait 6 :
prefers-reduced-motiona été déployé sur les grandes plateformes comme partie d’un effort d’accessibilité — la sensibilité au mouvement est un vrai problème, pas un simple réglage esthétique. - Fait 7 : Les écrans squelettes peuvent réduire le temps perçu d’attente plus efficacement que les spinners car ils montrent le progrès en « forme », même quand rien n’avance réellement.
- Fait 8 : Le Cumulative Layout Shift (CLS) est devenu central avec Core Web Vitals, changeant la définition d’une bonne UX de chargement pour le SEO et la rétention utilisateur.
- Fait 9 : L’accélération GPU n’est pas une baguette magique ; forcer la création de couches partout peut augmenter l’usage mémoire et provoquer plus de saccades quand le GPU manque de place.
Une citation à garder sur un post-it :
L’espoir n’est pas une stratégie.
— Gene Kranz.
Les écrans squelettes sont de l’espoir en CSS. Gardez-les honnêtes et mesurables.
Une base solide : blocs squelettes en pur CSS
Commencez par la version ennuyeuse. Vous voulez une base qui paraît acceptable même si l’animation est désactivée,
l’appareil est lent ou l’utilisateur a activé la réduction du mouvement. L’animation est une amélioration, pas le fondement.
Composant squelette minimal (sans lueur pour l’instant)
L’idée centrale : utilisez un fond neutre, des coins arrondis et des placeholders dimensionnés pour correspondre au contenu réel.
Évitez le sur-détail. Un squelette qui essaie d’imiter chaque nuance typographique est juste une seconde interface que vous devez maintenant maintenir.
cr0x@server:~$ cat skeleton.css
:root {
--sk-bg: #e9ecef;
--sk-fg: #f8f9fa;
--sk-radius: 10px;
}
.skeleton {
background: var(--sk-bg);
border-radius: var(--sk-radius);
position: relative;
overflow: hidden;
}
.skeleton.line { height: 1em; }
.skeleton.line.sm { height: 0.8em; }
.skeleton.line.lg { height: 1.2em; }
.skeleton.avatar {
width: 48px;
height: 48px;
border-radius: 999px;
}
.skeleton.block {
height: 160px;
}
.skeleton + .skeleton { margin-top: 12px; }
Cela vous donne des placeholders qui ne bougent pas, ne scintillent pas et ne consomment pas le CPU. Cela signifie aussi que votre histoire pour reduced-motion est déjà acceptable.
Réservez l’espace de mise en page pour éviter le CLS
Le CLS n’est pas qu’un problème d’image. C’est aussi « le squelette ne correspond pas à la mise en page réelle » et « les polices s’affichent tardivement ».
Les squelettes doivent réserver le même espace que le contenu chargé : mêmes hauteurs, mêmes marges, mêmes colonnes de grille.
Mesurez l’UI réelle et reproduisez-la.
Si vous ne connaissez pas les hauteurs exactes, utilisez des contraintes : ratios d’aspect pour les médias, min-heights pour les cartes et empilements de lignes pour le texte.
Et n’ôtez pas les squelettes avec display: none juste avant l’apparition du contenu si cela provoque une orgie de reflow.
Lueur : comment ça marche, et comment le faire sans faire surchauffer les ordinateurs
La lueur est la surbrillance mobile qui dit « chargement ». C’est aussi la façon la plus simple d’animer accidentellement un dégradé repassant
sur 40 placeholders et de se demander pourquoi le défilement ressemble à tirer un canapé.
Approche A (commune) : animer un dégradé de fond
C’est le snippet classique que vous avez vu partout : un dégradé linéaire qui se déplace à travers l’élément.
Cela peut convenir pour de petites surfaces, mais cela repainte souvent à chaque frame, surtout si la zone scintillante est large.
cr0x@server:~$ cat shimmer-gradient.css
.skeleton.shimmer {
background: linear-gradient(90deg, var(--sk-bg) 25%, var(--sk-fg) 37%, var(--sk-bg) 63%);
background-size: 400% 100%;
animation: sk-shimmer 1.2s ease-in-out infinite;
}
@keyframes sk-shimmer {
0% { background-position: 100% 0; }
100% { background-position: 0 0; }
}
Le risque : de larges repaints. Les dégradés ne sont pas juste une couleur ; ce sont une image rendue. Quand vous animez la position, le navigateur repainte souvent.
Parfois il peut être optimisé ; parfois non. Ne misez pas la performance de votre défilement sur « parfois ».
Approche B (recommandée) : animer une pseudo-élément overlay avec transform
Voici l’approche plus adaptée à la production : le squelette a un fond plat. Un pseudo-élément dessine la bande de surbrillance.
Vous animez le pseudo-élément avec transform. Cela donne au compositor une vraie chance de le faire sans repainter l’élément entier.
cr0x@server:~$ cat shimmer-transform.css
.skeleton.shimmer {
background: var(--sk-bg);
}
.skeleton.shimmer::after {
content: "";
position: absolute;
inset: 0;
transform: translateX(-100%);
background: linear-gradient(
90deg,
rgba(255,255,255,0) 0%,
rgba(255,255,255,0.55) 50%,
rgba(255,255,255,0) 100%
);
animation: sk-sweep 1.3s ease-in-out infinite;
will-change: transform;
}
@keyframes sk-sweep {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
Ce n’est pas garanti d’éviter le paint partout (les navigateurs varient), mais cela réduit généralement le périmètre d’impact.
La surbrillance devient un candidat pour une couche séparée. Le fond de base reste stable.
Première blague (nous en avons exactement deux, alors profitez-en avec modération) : Les loaders squelettes sont comme les invitations de réunion — s’ils durent trop longtemps, tout le monde suppose que quelque chose est cassé.
Gardez la lueur subtile et de courte durée
La lueur n’est pas un néon. Utilisez un contraste faible et une durée autour de 1.1–1.6 secondes. Plus rapide paraît frénétique. Plus lent paraît bloqué.
Et quand le contenu arrive, arrêtez l’animation immédiatement. Ne laissez pas la lueur tourner derrière le contenu chargé parce que vous avez oublié d’enlever une classe.
Ne faites pas scintiller tout
Si vous avez une liste de 30 lignes, animer chaque ligne est excessif. Faites scintiller les premières lignes ou un bloc représentatif.
L’utilisateur n’a pas besoin de 30 dégradés synchronisés pour comprendre « chargement ». Votre CPU, lui, en a besoin.
Staggering : esthétique plaisante, dangereux pour la performance
Les designers adorent les animations en décalage. Les SRE aiment des graphiques sans motifs en dents de scie. Le stagger peut être très beau,
mais il peut aussi empêcher les optimisations du navigateur car chaque élément est dans une phase d’animation différente.
Si vous avez beaucoup de squelettes, préférez une timeline d’animation partagée. Si vous devez stagner, faites-le légèrement et plafonnez le nombre.
Réduction du mouvement : respecter les utilisateurs sans expédier une interface « morte »
La réduction du mouvement n’est pas optionnelle. Certains utilisateurs ont des nausées ou des migraines à cause du mouvement constant. D’autres sont sur des appareils à faible consommation et veulent juste que la page arrête de gigoter.
Votre rôle est de fournir une expérience équivalente : clairement « chargement », mais sans balayages animés.
Utilisez prefers-reduced-motion pour désactiver la lueur
cr0x@server:~$ cat reduced-motion.css
@media (prefers-reduced-motion: reduce) {
.skeleton.shimmer::after {
animation: none;
opacity: 0.0;
}
.skeleton.shimmer {
background: var(--sk-bg);
}
}
Cela garde le placeholder visible, mais supprime la surbrillance mobile. Les utilisateurs voient toujours la structure et l’espace réservé.
Ils n’obtiennent simplement pas l’indice animé. C’est acceptable.
Envisagez un indice non animé
Si vous voulez fournir un indice « toujours vivant » sans mouvement, vous pouvez faire une pulsation lente et de faible contraste sur l’opacité.
Mais si l’utilisateur a demandé réduction du mouvement, la « pulsation » compte toujours comme mouvement pour beaucoup de personnes. Gardez-la désactivée en mode réduit.
Détails d’accessibilité que l’on oublie
- Lecteurs d’écran : les squelettes ne doivent pas être lus comme du contenu réel. Utilisez
aria-hidden="true"sur les blocs purement décoratifs. - Focus : ne placez pas d’éléments focalisables à l’intérieur des conteneurs squelettes. L’ordre de tabulation ne doit pas faire traverser les placeholders aux utilisateurs.
- Contraste des couleurs : les squelettes ne doivent pas ressembler à du texte désactivé réel. Ce sont des placeholders, pas du contenu « grisé ».
Modèle de performance : paint, composite et pourquoi les dégradés coûtent cher
Si vous voulez des écrans squelettes qui ne provoquent pas de saccades, vous avez besoin d’un modèle de rendu basique en tête.
Pas la version académique. La version « ce qui casse à 9:42 lors d’un pic de trafic ».
Ce que fait réellement le navigateur
- Style : calculer les règles CSS.
- Layout : calculer tailles et positions.
- Paint : dessiner des pixels dans des couches.
- Composite : déplacer les couches et les combiner dans la frame finale.
Les écrans squelettes vous nuisent quand ils provoquent :
- Thrash de layout : le squelette change constamment de taille ou bascule d’une manière qui déclenche le relayout.
- Paint coûteux : grands dégradés, filtres blur, box-shadows ou masques qui doivent être repaintés chaque frame.
- Explosion de couches : trop de couches promues (via
will-changeou animation), provoquant de la pression mémoire et de la perte de performance.
Règles empiriques qui tiennent en production
- Animez des transforms, pas background-position, quand vous pouvez. Transform reste souvent dans le compositor.
- Gardez les surfaces scintillantes petites. Un shimmer plein-viewport est une invitation aux frames perdues.
- Utilisez
content-visibilityavec précaution. Ça peut accélérer le rendu hors-écran, mais aussi créer des « pop-in » surprises si vous n’avez pas dimensionné les placeholders. - Ne spammez pas
will-change. Appliquez-le seulement aux éléments que vous animez et seulement pendant l’animation.
Quand les squelettes sont le mauvais outil
Si votre contenu est très variable, les squelettes peuvent induire en erreur. Exemple : un feed où les cartes peuvent faire 2 lignes ou 20 lignes.
Dans ce cas, utilisez un placeholder plus simple (un bloc par carte) ou réservez l’espace réel avec des min-heights.
Et si votre temps de chargement est principalement dû au décodage d’images ou au swap de polices, les squelettes ne résoudront pas le délai fondamental.
Ils resteront là, scintillant poliment, pendant que l’appareil lutte.
Playbook de diagnostic rapide
Quand les animations squelettes semblent « bizarres », vous pouvez perdre des heures au mauvais endroit. Voici l’ordre qui trouve généralement le coupable rapidement.
Première étape : confirmer quel type de lenteur c’est
- Est-ce lié au CPU ? Le ventilateur monte en régime, DevTools montre de longs « Recalculate Style »/« Paint ».
- Est-ce lié au GPU ? Le framerate chute pendant l’animation, surtout sur écrans à haute densité ou quand de nombreuses couches sont présentes.
- Le thread principal est-il bloqué par du JS ? Le squelette saccade quand l’analytics ou l’hydratation se déclenche.
- Est-ce réseau ? La durée du squelette est longue ; l’animation est fluide mais dure des secondes.
Deuxième étape : localiser les dégâts
- Désactivez la classe d’animation du squelette et rechargez : la saccade disparaît-elle ?
- Réduisez le nombre d’items squelette (ex. 30 → 5) : cela évolue linéairement ou y a-t-il un effondrement ?
- Changez l’implémentation du shimmer (background-position vs pseudo-element transform) : le temps de paint diminue ?
Troisième étape : valider avec des outils, pas des impressions
- Utilisez le profileur performance du navigateur pour identifier les hotspots layout/paint/composite.
- Utilisez des métriques OS CPU/GPU pour détecter throttling ou limites thermiques.
- Vérifiez les régressions CLS : le squelette doit stabiliser la mise en page, pas la déstabiliser.
Tâches pratiques avec commandes : mesurer, vérifier, décider
Ce sont des tâches pratiques que vous pouvez exécuter sur une machine de dev ou un hôte de test pour diagnostiquer les problèmes de performance des squelettes.
Chaque tâche inclut la commande, ce que signifie la sortie et la décision à prendre.
Pas d’héroïsme. Juste des preuves.
Task 1: Verify reduced-motion behavior at the OS level (GNOME)
cr0x@server:~$ gsettings get org.gnome.desktop.interface enable-animations
true
Ce que cela signifie : true signifie que les animations système sont autorisées ; false correspond généralement à une préférence de réduction du mouvement.
Décision : Si des utilisateurs signalent des problèmes de mouvement, reproduisez avec les animations désactivées et confirmez que votre chemin CSS prefers-reduced-motion se comporte comme attendu.
Task 2: Check CPU pressure during skeleton animation (Linux)
cr0x@server:~$ pidstat -dur 1 5
Linux 6.8.0 (server) 12/29/2025 _x86_64_ (8 CPU)
# Time UID PID %usr %system %guest %CPU CPU kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
12:10:01 1000 24138 38.00 4.00 0.00 42.00 3 0.00 12.00 0.00 0 chrome
Ce que cela signifie : Chrome consomme environ 42% du CPU pendant la fenêtre mesurée.
Décision : Si les pics CPU coïncident avec le shimmer, passez à un shimmer basé sur transform, réduisez le nombre de squelettes ou stoppez l’animation hors-écran.
Task 3: Identify if you’re GPU-throttled (Linux + Intel/AMD)
cr0x@server:~$ sudo intel_gpu_top -s 1000
intel_gpu_top - Intel(R) Graphics - Frequency 600MHz - 0.00/ 0.00 Watts
IMC reads: 4121 MiB/s writes: 623 MiB/s
Render/3D: 78.21% Blitter: 0.00% Video: 0.00%
Ce que cela signifie : Le moteur Render/3D est occupé (~78%). Votre animation peut forcer un composite lourd ou de grandes couches texturées.
Décision : Réduisez le nombre de couches (évitez will-change partout), réduisez les zones scintillantes et évitez les dégradés pleine largeur sur de nombreux éléments.
Task 4: Confirm whether Chrome is using GPU acceleration
cr0x@server:~$ google-chrome --version
Google Chrome 121.0.6167.160
Ce que cela signifie : Vous avez une version Chrome connue ; vous pouvez maintenant reproduire de façon cohérente sur différentes machines.
Décision : Figez vos étapes de reproduction sur une version de navigateur. Les régressions d’animation peuvent être spécifiques à une version ; traitez-la comme une dépendance.
Task 5: Capture a quick headless performance trace using Playwright
cr0x@server:~$ node -e "const { chromium } = require('playwright'); (async () => { const b = await chromium.launch(); const p = await b.newPage(); await p.tracing.start({ screenshots: false, snapshots: true }); await p.goto('http://localhost:8080'); await p.waitForTimeout(4000); await p.tracing.stop({ path: 'trace.zip' }); await b.close(); })();"
Ce que cela signifie : Vous avez produit trace.zip, qui peut être inspecté pour voir l’activité layout/paint dans le temps.
Décision : Si vous voyez des événements de paint fréquents pendant le shimmer, préférez le balayage par transform ou réduisez la zone affectée.
Task 6: Check if the device is thermally throttling (Linux)
cr0x@server:~$ sensors
coretemp-isa-0000
Adapter: ISA adapter
Package id 0: +92.0°C (high = +100.0°C, crit = +100.0°C)
Core 0: +90.0°C (high = +100.0°C, crit = +100.0°C)
Ce que cela signifie : L’appareil chauffe. Le thermal throttling peut rendre les animations saccadées même si votre CSS est « correct ».
Décision : Testez sur un appareil froid et un appareil chaud. Si le shimmer saccade seulement à chaud, réduisez le coût d’animation et stoppez l’animation hors-écran.
Task 7: Inspect frame drops via a simple FPS counter overlay (Chrome flag-free approach)
cr0x@server:~$ node -e "console.log('Open DevTools -> Rendering -> enable FPS meter. Watch for drops during skeleton shimmer.');"
Open DevTools -> Rendering -> enable FPS meter. Watch for drops during skeleton shimmer.
Ce que cela signifie : Vous utilisez le compteur FPS intégré pour voir si vous êtes à 60fps/120fps ou en train de chuter.
Décision : Si le FPS chute pendant le shimmer mais pas avec des squelettes statiques, l’animation est la cause. Changez la stratégie d’animation ou sa portée.
Task 8: Audit layout shift with Lighthouse CLI (local)
cr0x@server:~$ lighthouse http://localhost:8080 --only-categories=performance --output=json --quiet --chrome-flags="--headless" | jq '.audits["cumulative-layout-shift"].numericValue'
0.19
Ce que cela signifie : CLS est à 0.19, ce qui n’est pas bon. Les squelettes peuvent ne pas correspondre à la mise en page finale, ou des images/polices provoquent des décalages.
Décision : Corrigez l’espace réservé : hauteurs/ratios explicites, faites correspondre les dimensions du squelette à l’UI finale et contrôlez le comportement de chargement des polices.
Task 9: Confirm font swap behavior contributing to shift
cr0x@server:~$ rg -n "font-display" -S .
assets/css/fonts.css:12: font-display: swap;
Ce que cela signifie : Les polices utilisent swap. Cela peut provoquer un décalage si les métriques de secours diffèrent fortement.
Décision : Si le CLS est élevé, envisagez des piles de polices compatibles métriquement et assurez-vous que les hauteurs de ligne des squelettes correspondent aux métriques du texte final.
Task 10: Detect whether you’re shipping excessive skeleton CSS
cr0x@server:~$ gzip -c dist/app.css | wc -c
48219
Ce que cela signifie : Le CSS compressé fait ~48KB. Si les styles de squelette représentent une grande part, c’est du poids et du temps de parsing payés sur chaque route.
Décision : Séparez le CSS critique. Gardez les styles squelette petits, réutilisables et évitez de générer des centaines de classes sur-mesure pour chaque forme de placeholder.
Task 11: Check for accidental infinite animation on loaded content
cr0x@server:~$ rg -n "shimmer|skeleton" dist/app.js dist/app.css
dist/app.css:44:.skeleton.shimmer::after { animation: sk-sweep 1.3s ease-in-out infinite; will-change: transform; }
dist/app.js:221:document.body.classList.add("loading")
Ce que cela signifie : Vous ajoutez encore une classe loading globale. Si elle n’est pas retirée de manière fiable, l’animation peut continuer à tourner.
Décision : Ajoutez un timeout hard et un nettoyage basé sur l’état. Ciblez aussi le shimmer uniquement sur les éléments squelette, pas sur la page entière.
Task 12: Ensure you aren’t creating thousands of layers via will-change
cr0x@server:~$ rg -n "will-change" -S dist/app.css
44: will-change: transform;
101: will-change: transform;
102: will-change: opacity;
103: will-change: transform, opacity;
Ce que cela signifie : Plusieurs usages de will-change. S’il est appliqué largement (ex. sur chaque item de liste), cela peut provoquer une pression mémoire.
Décision : Ciblez will-change sur un petit ensemble d’éléments et retirez-le quand l’animation s’arrête. Ne « optimisez » pas avec une promotion permanente de couches.
Task 13: Verify that skeleton items stop animating when offscreen (basic check)
cr0x@server:~$ node -e "console.log('If you have a long list, scroll: does CPU stay high? If yes, consider pausing animation offscreen via IntersectionObserver toggling a class.');"
If you have a long list, scroll: does CPU stay high? If yes, consider pausing animation offscreen via IntersectionObserver toggling a class.
Ce que cela signifie : C’est un test comportemental : le CPU doit baisser quand les éléments animés sortent du viewport.
Décision : Si le CPU reste élevé, mettez en pause le shimmer hors-écran. Le CSS pur ne peut pas détecter la visibilité de façon fiable, vous aurez besoin d’un peu de JS pour basculer des classes.
Task 14: Check whether the shimmer is causing repaint storms (X11 tooling)
cr0x@server:~$ xrestop -b | head
res-base Wins pixmap Other Total Pid Name
623K 35 1816K 4102K 6541K 24138 chrome
Ce que cela signifie : Si la mémoire pixmap grimpe rapidement pendant que la lueur tourne, vous pouvez générer de grandes surfaces/couches.
Décision : Réduisez la zone de shimmer, évitez les gros dégradés et diminuez le nombre de squelettes animés simultanément.
Deuxième blague (et c’est tout) : Ajouter will-change partout, c’est comme étiqueter toutes les boîtes « FRAGILE » — ça n’accélère pas l’expédition, ça énerve tout le monde.
Trois mini-histoires du monde corporate
Mini-histoire 1 : L’incident causé par une mauvaise hypothèse
Une équipe produit a livré une « nouvelle expérience de chargement » pour un tableau de bord. C’était élégant : cartes squelettes avec lueur, offsets en stagger et un blur subtil.
L’hypothèse était simple : l’animation CSS est moins coûteuse qu’un spinner JS, donc ça doit être sûr. Personne ne l’a profilée sur du matériel bas de gamme car les laptops de staging étaient tous haut de gamme.
Le jour de la mise en production, les tickets support ont afflué décrivant des « gel du scroll » et « drain de batterie ». Les métriques semblaient normales : latence backend stable.
Le taux de cache/CDN était normal. Pourtant les sessions utilisateurs sur certains appareils avaient une durée moindre et plus de rage clicks.
Le thread principal n’était pas bloqué par du JS ; il était noyé dans du travail de paint.
La lueur utilisait des dégradés de fond animés sur chaque carte — des dizaines à la fois — plus un filtre blur pour « adoucir » la surbrillance.
Le navigateur repaintait de grandes zones à chaque frame. Sur des GPU faibles, le composite tombait dans des chemins moins efficaces.
Le résultat était le genre de saccade qu’on ressent jusque dans les dents.
La correction fut presque embarrassante : supprimer le blur, arrêter d’animer les positions de dégradé, et animer un pseudo-élément en transform sur seulement quelques cartes visibles.
Ils ont aussi désactivé la lueur en reduced-motion et en modes basse consommation.
L’UI était un peu moins fancy, et le produit a soudainement cessé de faire chauffer les téléphones.
Mini-histoire 2 : L’optimisation qui s’est retournée contre eux
Une autre équipe a voulu « résoudre » la performance du shimmer en forçant l’accélération GPU. Ils ont ajouté will-change: transform à chaque élément squelette,
plus quelques autres composants « par précaution ». En local, le FPS initial s’est amélioré. L’équipe a célébré et a mergé le changement.
Une semaine plus tard, des saccades intermittentes — pires qu’avant — apparaissaient sur de longues pages.
Les utilisateurs signalaient que changer d’onglet et revenir entraînait un redraw lent.
Sur certains appareils, le navigateur affichait même brièvement des rectangles noirs pendant le scroll. Pas souvent. Juste assez pour ruiner la confiance.
Le problème n’était pas que will-change est mauvais. Le problème était l’échelle. Promouvoir trop d’éléments en couches séparées augmente l’usage mémoire et la surcharge de gestion.
Quand le système manque de mémoire GPU, le compositor doit jongler avec les surfaces.
L’« optimisation » est devenue un thrash de couches.
Le rollback fut chirurgical : appliquer will-change seulement au pseudo-élément highlight, seulement pendant son animation, et uniquement pour les squelettes above-the-fold.
Ils ont aussi réduit le nombre de squelettes sur les longues listes en n’affichant que quelques placeholders jusqu’au scroll.
La performance moyenne est redevenue ennuyeuse, ce qui est le plus beau compliment pour une UI en production.
Mini-histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la situation
Une appli de paiement devait livrer une refonte sous des contraintes de fiabilité strictes : tout glitch UI durant le checkout était traité comme incident critique.
L’équipe a insisté sur une approche checklist-first. Pas glamour. Efficace.
Ils ont défini des contrats de squelettes par composant : hauteurs exactes, espacements et ratios d’image réservés.
Ils ont écrit une petite suite de régression visuelle qui capturait des captures avant/après autour des transitions de chargement.
Ils exécutaient aussi un contrôle Lighthouse automatique qui faisait échouer le CI si le CLS dépassait un seuil sur les flux clés.
Lors d’une release, un tweak typographique apparemment inoffensif a changé la hauteur de ligne et rendu le contenu réel plus grand que le squelette.
La suite visuelle l’a signalé immédiatement. La régression CLS n’a jamais atteint la production.
L’équipe a ajusté les empilements de lignes des squelettes et mis à jour les polices de fallback métriquement compatibles.
Rien de dramatique ne s’est passé. Pas d’alerte incident. Pas d’email d’excuse. Juste une continuité silencieuse.
Le meilleur travail de fiabilité se lit souvent comme un non-événement parce qu’il empêche l’événement d’exister.
Erreurs fréquentes : symptôme → cause racine → correction
1) Symptom: scrolling stutters only while skeletons are visible
Cause racine : lueur implémentée via des dégradés de fond animés sur de nombreux grands éléments, provoquant des repaints fréquents.
Correction : passez à un balayage pseudo-élément animé via transform ; réduisez le nombre de squelettes animés ; évitez blur et ombres lourdes.
2) Symptom: skeleton looks fine, but CLS is still high
Cause racine : les dimensions du squelette ne correspondent pas à la mise en page finale (métriques de police différentes, ratio d’image manquant, hauteur de contenu variable).
Correction : réservez l’espace exact en utilisant des hauteurs cohérentes, des boîtes aspect-ratio et des empilements de lignes ; assurez-vous que le squelette et le composant final partagent les mêmes règles de layout.
3) Symptom: animation keeps running after content loads
Cause racine : la classe loading n’est pas retirée de façon fiable (changement de route, chemins d’erreur, requêtes abortées), ou le shimmer est appliqué globalement.
Correction : liez la visibilité du squelette à l’état ; ajoutez un nettoyage sur succès, erreur et abort ; scopez le shimmer aux éléments squelette seulement.
4) Symptom: reduced-motion users still see shimmer
Cause racine : règles @media (prefers-reduced-motion) manquantes ou écrasées ; shimmer implémenté d’une façon non couverte par l’override.
Correction : désactivez explicitement les keyframes et retirez l’overlay pseudo-élément en mode réduit ; vérifiez avec les paramètres OS et l’émulation du navigateur.
5) Symptom: performance is fine on desktop, awful on mobile
Cause racine : les GPU mobiles et les limites thermiques sont plus contraints ; les écrans haute densité augmentent le coût de paint/composite ; trop d’éléments animés simultanément.
Correction : limitez l’animation aux placeholders above-the-fold ; mettez en pause le shimmer hors-écran ; réduisez le contraste et la largeur de la bande scintillante.
6) Symptom: skeleton flickers or shows seams during animation
Cause racine : artefacts de rendu sous-pixel dus aux transforms, surtout sur des éléments scalés ou des largeurs fractionnelles.
Correction : utilisez des dimensions en pixels entiers quand c’est possible ; évitez de scaler les conteneurs squelettes ; envisagez transform: translate3d(...) uniquement si cela apporte un réel bénéfice sur vos navigateurs cibles.
7) Symptom: CPU usage stays high even when page is idle
Cause racine : animations infinies sur de nombreux éléments ; aucune condition d’arrêt ; animations hors-écran ; l’onglet n’est pas throttlé à cause d’audio/vidéo actif ou autres facteurs.
Correction : arrêtez la lueur quand les données arrivent ; utilisez un JS minimal pour mettre en pause les animations hors-écran ; réduisez le nombre de squelettes et évitez le shimmer global.
8) Symptom: skeleton “feels slower” than a spinner
Cause racine : le squelette apparaît mais le contenu prend tellement de temps que la lueur devient un aimant d’attention ; l’utilisateur le perçoit comme « bloqué ».
Correction : réduisez l’intensité du shimmer, ajoutez un rendu progressif du vrai contenu et corrigez la performance backend/cache réelle. Les squelettes doivent couvrir une courte incertitude, pas une longue souffrance.
Listes de contrôle / plan étape par étape
Étape par étape : livrer un loader squelette que vous ne regretterez pas
- Inventaire des états de chargement. Identifiez où les utilisateurs attendent : premier chargement, transitions de route, chargements partiels de composants.
- Définissez des contrats de composant. Pour chaque squelette, spécifiez hauteur, comportement de largeur, espacements et quelles parties scintillent (le cas échéant).
- Commencez statique. Construisez les squelettes sans animation. Confirmez la stabilité du layout et un rendu acceptable.
- Ajoutez la lueur comme amélioration. Préférez le balayage pseudo-élément avec transform.
- Respectez la réduction du mouvement. Désactivez la lueur sous
prefers-reduced-motion; gardez les placeholders visibles. - Limitez les animations simultanées. Laissez scintiller seulement above-the-fold ou les N premières lignes d’une liste.
- Arrêtez rapidement l’animation. Retirez la classe shimmer immédiatement quand le contenu est prêt ; gérez aussi les chemins d’erreur et d’abort.
- Mesurez CLS et FPS. Lancez Lighthouse (CLS) et un profil de performance (activité paint/composite).
- Testez sur appareils contraints. Anciens téléphones, conditions thermiques, modes économie d’énergie — réaliste, pas idéal.
- Automatisez les régressions. Ajoutez un contrôle CI pour le CLS et au moins une trace de performance pour la route lourde en squelettes.
Checklist : garde-fous de performance
- N’animez pas
filterou de grosbox-shadowsur les squelettes. - N’animez pas de grands dégradés de fond sur de larges surfaces sauf après profilage confirmé.
- N’utilisez pas
will-changepartout ; ciblez-le et retirez-le après chargement. - Ne faites pas scintiller le contenu hors-écran ; mettez-le en pause.
- Faites correspondre les dimensions du squelette à la mise en page finale pour garder le CLS bas.
Checklist : garde-fous d’accessibilité
- Les blocs squelettes sont décoratifs : cachez-les aux lecteurs d’écran sauf s’ils transmettent un état significatif.
- Ne piègez jamais le focus dans un état squelette.
- Respectez
prefers-reduced-motionde façon cohérente à travers les composants. - Gardez le contraste des squelettes subtil ; ils ne doivent pas se faire passer pour du texte désactivé.
FAQ
1) Les écrans squelettes sont-ils meilleurs que les spinners ?
En général, oui pour les interfaces riches en contenu. Les squelettes communiquent la structure et réduisent la latence perçue.
Les spinners conviennent pour de courtes attentes ou des tâches non liées à la mise en page (ex. sync en arrière-plan), mais ils ne réservent pas l’espace et peuvent donner l’impression d’un « attendez » sans contexte.
2) Puis-je faire des loaders squelettes sans aucun JavaScript ?
Vous pouvez rendre des squelettes en pur CSS et les retirer côté serveur quand le contenu est prêt. Mais dans les apps rendues côté client, vous aurez typiquement besoin d’un petit toggle d’état pour retirer les classes squelette.
De plus, mettre en pause le shimmer hors-écran nécessite du JS (ex. IntersectionObserver) car le CSS ne peut pas détecter la visibilité viewport de façon fiable.
3) Pourquoi mon shimmer cause-t-il un CPU élevé alors que c’est « juste du CSS » ?
Parce que vous forcez peut-être des repaints. Animer la position des dégradés, des filtres blur et de grandes zones de repaint coûte du CPU/GPU à chaque frame.
Préférez les animations basées sur transform et gardez la surface animée petite.
4) will-change est-il toujours bon pour le shimmer ?
Non. Il peut aider pour un petit nombre d’éléments animés en permettant la promotion en couche.
Mais appliqué largement, il augmente l’usage mémoire et peut provoquer du thrash de couches. Utilisez-le avec parcimonie et retirez-le quand c’est fini.
5) Combien d’items squelette doivent scintiller en même temps ?
Assez pour indiquer le « chargement », pas assez pour chauffer la salle. Pour les listes, faites scintiller 3–6 items above-the-fold et laissez le reste statique.
Si vous voulez une règle stricte : commencez par 4 et n’augmentez qu’après profilage sur appareils bas de gamme.
6) Quelle est la meilleure durée pour le shimmer ?
Environ 1.1–1.6 secondes par balayage semble naturel. Plus rapide paraît nerveux ; plus lent paraît bloqué.
Plus important que la durée est d’arrêter l’animation immédiatement une fois le contenu prêt.
7) Comment les écrans squelettes interagissent-ils avec Core Web Vitals ?
Bien faits, les squelettes réduisent le CLS en réservant l’espace. Mal faits, ils peuvent augmenter le CLS s’ils ne correspondent pas à la mise en page finale.
Ils n’améliorent pas directement le LCP sauf s’ils vous aident à rendre du contenu significatif plus tôt ; ils améliorent surtout la performance perçue.
8) Les squelettes doivent-ils reproduire exactement les lignes de texte et la typographie ?
Reproduisez la structure, pas les détails. Utilisez quelques blocs de ligne avec des largeurs réalistes et un espacement cohérent.
Des squelettes trop précis sont fragiles : tout changement de texte ou de typographie devient un décalage de layout et un coût de maintenance.
9) Quelle est l’approche réduite-motion la plus simple ?
Sous prefers-reduced-motion: reduce, désactivez l’animation keyframe et retirez l’overlay scintillant.
Laissez les placeholders statiques. C’est respectueux et toujours informatif.
10) Comment savoir si mon shimmer est bound au paint ou au composite ?
Profilez-le. Si vous voyez une activité « Paint » fréquente pendant le shimmer, vous êtes bound au paint. Si le paint est faible mais que le GPU est occupé et que vous avez beaucoup de couches, vous pouvez être bound au composite.
La correction pratique est similaire : réduisez la zone animée et le nombre d’éléments, et utilisez des animations basées sur transform.
Conclusion : prochaines étapes pratiques
Les écrans squelettes valent le coup, mais seulement si vous les traitez comme du code de production, pas comme une décoration.
Construisez d’abord une base statique. Ajoutez la lueur seulement là où elle aide. Respectez la réduction du mouvement. Et profilez sur du matériel qui n’a pas un ventilateur de la taille d’un dessert.
Prochaines étapes que vous pouvez exécuter cette semaine :
- Remplacez le shimmer par position de dégradé par un balayage pseudo-élément basé sur transform sur votre composant squelette principal.
- Ajoutez des overrides
prefers-reduced-motionet vérifiez-les avec les paramètres système. - Limitez le shimmer aux placeholders above-the-fold et arrêtez-le immédiatement quand les données sont prêtes.
- Exécutez Lighthouse pour le CLS et une trace de performance sur une route avec longue liste ; échouez le CI en cas de régressions évidentes.
Vos utilisateurs n’enverront probablement pas de mails de remerciement pour un chargement fluide. Ils cesseront juste de se plaindre. C’est le rêve.