Vous avez livré la refonte. L’équipe produit l’adore. Puis les tableaux de bord s’illuminent : Core Web Vitals « À améliorer », la conversion baisse, et le support client signale « ça donne l’impression d’être lent », comme si c’était un seul bug qu’on peut greper.
C’est le moment où les équipes gaspillent des semaines à polir le mauvais caillou. Les Core Web Vitals se mesurent, mais ce n’est pas magique. Traitez-les comme tout SLO de production : comprenez ce qui échoue vraiment, isolez le goulet d’étranglement, et déployez le plus petit correctif qui change la courbe.
Core Web Vitals en termes simples (sans encens)
Les Core Web Vitals (CWV) sont un petit ensemble de métriques centrées sur l’utilisateur. Ce n’est pas « la sauce secrète de Google ». C’est une manière standardisée de décrire : (1) à quelle vitesse un contenu significatif apparaît, (2) la stabilité visuelle pendant le chargement, et (3) la réactivité perçue quand l’utilisateur interagit.
LCP : Largest Contentful Paint (chargement)
LCP répond à la question : « Quand l’utilisateur a-t-il vu la chose principale ? » Généralement une image hero, un grand titre ou le bloc de contenu supérieur. Ce n’est pas « quand le spinner démarre ». Ce n’est pas « DOMContentLoaded ». C’est le plus grand élément visible dans la fenêtre d’affichage.
Tueurs courantes du LCP :
- TTFB élevé (origine lente, ratés de cache, SSR coûteux, mauvaise configuration du CDN)
- CSS bloquant le rendu
- Images hero non prioritaires (lazy-loaded au-dessus de la ligne de flottaison, pas de preload, octets énormes)
- Rendu côté client qui retarde le contenu jusqu’à l’exécution du JS
- Scripts tiers qui monopolisent le thread principal tôt
CLS : Cumulative Layout Shift (stabilité visuelle)
CLS répond à la question : « La page a-t-elle bougé sous le curseur de l’utilisateur ? » Les décalages de mise en page sont ces sauts frustrants quand des images, des pubs ou des polices qui chargent tard poussent le contenu. CLS ne concerne pas les animations ; il s’agit des mouvements inattendus.
Tueurs courantes du CLS :
- Images/iframes sans width/height (ou sans aspect-ratio)
- Bannières injectées (consentement cookies, rubans promo) qui poussent le contenu
- Swap de polices tardif qui reflowe le texte
- Ads/widgets qui redimensionnent après le chargement
INP : Interaction to Next Paint (réactivité)
INP répond à la question : « Quand l’utilisateur interagit, combien de temps avant que l’interface se mette à jour ? » Il a remplacé FID parce que les utilisateurs ne se préoccupent pas seulement de la première interaction ; ils veulent que le site reste réactif durant toute la session. INP est sensible aux tâches longues, à la contention du thread principal et aux gestionnaires d’événements coûteux.
Traduction opérationnelle : LCP est majoritairement un problème de pipeline de livraison (réseau + ressources critiques). CLS est surtout une question de discipline de mise en page. INP est principalement un problème d’ordonnancement CPU et d’hygiène du code.
Une citation, parce qu’elle tient aussi pour la perf web : Idée paraphrasée de John Ousterhout : la complexité est la cause racine de beaucoup de problèmes logiciels ; la réduire améliore la fiabilité et la performance.
Blague #1 : Le travail sur la performance, c’est comme un régime : les gens jurent par des astuces étranges, mais « manger moins » gagne toujours.
Ce qui fait réellement la différence : une pile de priorités
La plupart des équipes perdent du temps parce qu’elles traitent les CWV comme une liste de micro-optimisations. Ne le faites pas. Traitez-les comme une réponse à incident : identifiez la contrainte dominante, corrigez-la, re-mesurez, répétez.
1) Corrigez le chemin critique avant de toucher aux micro-optimisations
Si votre HTML met 800 ms à arriver, enlever 20 ms d’un bundle JS est du théâtre. Votre chemin critique est :
- DNS/TCP/TLS (parfois masqués par les CDN, parfois non)
- TTFB du HTML + taille du HTML
- CSS critique et ressources bloquantes
- Image hero (priorité + octets)
- Hydratation / exécution JS (pour les hybrides SPA/SSR)
Faites bouger le LCP en corrigeant la contrainte la plus précoce qui est lente.
2) Mettez en cache sérieusement (et vérifiez-le)
« On utilise un CDN » n’est pas la même chose que « notre HTML et les assets hero sont réellement servis depuis le cache pour les vrais utilisateurs ». Les CWV sont centrés utilisateur ; si 40 % des utilisateurs ratent le cache à cause des cookies, des en-têtes Vary ou du comportement géographique, vos moyennes seront mauvaises.
3) Arrêtez le lazy-loading pour les images au-dessus de la ligne de flottaison
Le lazy-loading est excellent pour le contenu en dessous de la ligne de flottaison. Au-dessus de la ligne, c’est souvent de l’auto-sabotage : vous dites au navigateur « ce n’est pas important », puis vous vous demandez pourquoi le LCP souffre.
4) Concevez la stabilité de la mise en page
Les corrections CLS sont souvent ennuyeuses : déclarez les dimensions, réservez de l’espace, n’injectez pas d’UI qui affecte la mise en page tardivement. Le gain est durable et ne dépend ni de l’appareil utilisateur, ni du réseau, ni de la chance.
5) Réduisez la dette du thread principal
INP, c’est votre facture « JavaScript est une taxe ». Vous la payez en tâches longues, frameworks lourds et scripts tiers. La correction n’est pas une astuce unique ; c’est de la discipline continue : bundles plus petits, moins d’observers, moins de re-renders, ordonnancement plus intelligent.
6) Considérez les scripts tiers comme des dépendances de production
Tags marketing, A/B testing, chat widgets, détection de fraude : ils s’exécutent sur le CPU de vos utilisateurs, pas sur le vôtre. Ils peuvent dominer l’INP et nuire au LCP s’ils s’exécutent tôt. Chargez-les plus tard, isolez-les ou supprimez-les. Oui, supprimez-les.
Blague #2 : Le script tiers le plus rapide est celui que votre service juridique a déjà approuvé pour suppression.
Faits et histoire qui expliquent le désordre actuel
- Fait 1 : « DOMContentLoaded » et « onload » sont devenus populaires parce qu’ils étaient faciles à mesurer, pas parce qu’ils reflétaient la perception utilisateur.
- Fait 2 : HTTP/2 a changé le compromis « un gros bundle vs plusieurs petits fichiers » en multiplexant les requêtes, mais le head-of-line blocking n’a pas disparu partout ; le transport compte toujours.
- Fait 3 : L’usage massif des SPA a déplacé les échecs de performance du réseau vers le CPU : les utilisateurs attendent le parsing/exécution du JS, pas seulement les octets.
- Fait 4 : Les polices web étaient autrefois une amélioration visuelle simple ; désormais elles représentent un risque de performance parce qu’elles affectent le rendu et peuvent déclencher des décalages de mise en page lors du swap.
- Fait 5 : L’ère « lazy-load everything » a été une réaction aux pages lourdes, mais elle a créé une nouvelle classe de bugs : différer exactement le contenu que les utilisateurs sont venus voir.
- Fait 6 : Google a introduit les Web Vitals pour pousser vers des métriques standardisées et centrées utilisateur ; l’industrie avait trop de définitions incompatibles de « rapide ».
- Fait 7 : INP a remplacé FID parce qu’optimiser seulement la première interaction laissait des pages « passer » tout en buguant durant l’usage réel.
- Fait 8 : L’instabilité de la mise en page s’est aggravée quand l’écosystème ad-tech a normalisé l’insertion dynamique de contenu ; CLS est essentiellement la métrification de la colère utilisateur.
- Fait 9 : Le RUM (real user monitoring) est devenu critique parce que les tests en laboratoire ne peuvent pas modéliser chaque appareil, réseau, limitation CPU ou extension.
Feuille de route pour un diagnostic rapide (premier/deuxième/troisième)
C’est la route la plus rapide vers le goulet d’étranglement quand une page échoue aux CWV. L’objectif : ne pas tout vouloir faire. Trouvez le limitateur dominant.
Premier : décidez si c’est LCP, CLS ou INP (et pour quelles pages)
- Utilisez le RUM pour identifier les URL/templates qui échouent le plus (pas seulement les moyennes).
- Segmentez par classe d’appareil (le mobile dit généralement la vérité en premier).
- Regardez le p75, pas la moyenne. Les CWV sont notés par percentiles.
Deuxième : pour le LCP, séparez serveur vs client vs octets
- TTFB élevé ? Corrigez la mise en cache, la latence d’origine, le coût du SSR, la configuration edge.
- TTFB correct mais LCP élevé ? Regardez le CSS bloquant le rendu, la priorité/taille de l’image hero, et les preload.
- L’élément LCP est du texte ? Vérifiez le comportement de chargement des polices et le blocage par le CSS.
- L’élément LCP est une image ? Corrigez le format/taille, les hints de priorité, la mise en cache, et évitez le décodage tardif.
Troisième : pour l’INP, trouvez les tâches longues et les handlers coupables
- Utilisez des traces pour identifier les tâches longues > 50 ms ; cherchez celles répétées.
- Identifiez les handlers d’événements lourds (click/input/keydown) et les tempêtes de re-render.
- Auditez les scripts tiers et les timers ; mesurez leur temps sur le thread principal.
Triage CLS : surveillez les injections tardives et les dimensions manquantes
- Trouvez les principales sources de shift ; ce sont généralement un petit nombre d’éléments.
- Corrigez en réservant de l’espace et en évitant d’insérer des éléments qui modifient la mise en page au-dessus de la ligne de flottaison.
Si vous ne pouvez pas répondre « quel est l’élément LCP pour ce template ? » en 10 minutes, vous ne faites pas d’ingénierie de performance. Vous faites du feeling.
Tâches pratiques : commandes, sorties, décisions
Ce sont des tâches ennuyeuses et exécutables que vous pouvez faire aujourd’hui. Chacune a : une commande, un exemple de sortie, ce que ça signifie, et la décision à prendre. Vous n’en avez pas besoin tous les jours ; vous avez besoin des bons quand vous êtes coincé.
Task 1: Measure TTFB and caching at the edge with curl
cr0x@server:~$ curl -s -o /dev/null -D - https://www.example.com/ | egrep -i 'HTTP/|cache-control|age|x-cache|server-timing|vary'
HTTP/2 200
cache-control: public, max-age=0, s-maxage=600
age: 512
x-cache: HIT
server-timing: cdn-cache;desc=HIT, edge;dur=12, origin;dur=0
vary: Accept-Encoding
Ce que ça signifie : age: 512 et x-cache: HIT suggèrent que la réponse est servie depuis le cache. server-timing indique qu’il n’y a pas de temps d’origine.
Décision : Si vous voyez MISS pour le trafic réel, corrigez les clés de cache (cookies, en-têtes Vary, params de requête) et les TTL edge avant de toucher au JS.
Task 2: Check time to first byte precisely with curl timings
cr0x@server:~$ curl -s -o /dev/null -w 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' https://www.example.com/
dns=0.012 connect=0.045 tls=0.089 ttfb=0.312 total=0.428
Ce que ça signifie : TTFB est de 312 ms. Total 428 ms. Le réseau n’est pas le principal coupable ici.
Décision : Si TTFB > ~800 ms sur des hits de cache, vous avez probablement une latence edge/origin ou un overhead de génération HTML. Corrigez cela en premier.
Task 3: Identify render-blocking resources with Lighthouse CI (headless)
cr0x@server:~$ npx lighthouse https://www.example.com/ --quiet --chrome-flags="--headless" --only-categories=performance --output=json --output-path=./lh.json
...Saved JSON report to ./lh.json...
cr0x@server:~$ jq '.audits["render-blocking-resources"].details.items[] | {url, totalBytes, wastedMs}' lh.json | head
{
"url": "https://www.example.com/assets/app.css",
"totalBytes": 184322,
"wastedMs": 410
}
{
"url": "https://www.example.com/assets/vendor.js",
"totalBytes": 912443,
"wastedMs": 280
}
Ce que ça signifie : Le CSS est volumineux et bloquant ; le vendor JS bloque aussi (probablement via des scripts synchrones ou un mauvais usage du preload).
Décision : Inlinez le CSS critique, séparez le CSS non critique, et assurez-vous que le JS est defer/async de façon appropriée. Ne faites pas « minifier plus fort » et appelez ça fini.
Task 4: Confirm the LCP element and its request chain (trace via Chrome DevTools Protocol)
cr0x@server:~$ npx chrome-har-capturer --url https://www.example.com/ --output ./page.har
Saved HAR to ./page.har
cr0x@server:~$ jq '.log.entries[] | select(.response.content.mimeType|test("image|text/html|text/css")) | {url: .request.url, status: .response.status, size: .response.content.size, wait: .timings.wait}' page.har | head
{
"url": "https://www.example.com/",
"status": 200,
"size": 62310,
"wait": 180
}
{
"url": "https://www.example.com/assets/app.css",
"status": 200,
"size": 184322,
"wait": 92
}
{
"url": "https://www.example.com/images/hero.jpg",
"status": 200,
"size": 1452200,
"wait": 210}
Ce que ça signifie : L’image hero fait 1,45 Mo et attend 210 ms avant le premier octet. C’est un classique point d’ancrage LCP.
Décision : Convertissez la hero en AVIF/WebP, redimensionnez, servez des variantes responsives, et assurez-vous qu’elle est demandée tôt (pas de lazy-load, envisagez le preload).
Task 5: Inspect cacheability and compression of the hero image
cr0x@server:~$ curl -s -I https://www.example.com/images/hero.jpg | egrep -i 'content-type|content-length|cache-control|etag|accept-ranges|content-encoding'
content-type: image/jpeg
content-length: 1452200
cache-control: public, max-age=3600
etag: "a9d1-5f2c9d3f"
accept-ranges: bytes
Ce que ça signifie : C’est un JPEG, volumineux, et mis en cache pour une heure. La mise en cache est correcte ; l’encodage ne l’est pas.
Décision : Envoyez des formats modernes et des dimensions plus petites. Le cache ne sauvera pas les visiteurs de première visite.
Task 6: Verify HTML is not accidentally uncacheable due to cookies/Vary
cr0x@server:~$ curl -s -I https://www.example.com/ | egrep -i 'set-cookie|vary|cache-control'
cache-control: private, no-store
set-cookie: session=...; Path=/; Secure; HttpOnly
vary: Cookie
Ce que ça signifie : Vous avez dit à tous les caches de se pousser. C’est une taxe TTFB sur chaque utilisateur.
Décision : Séparez le contenu personnalisé de la shell cacheable. Évitez Vary: Cookie sur le HTML sauf si vous êtes prêt à en payer le prix.
Task 7: Find long tasks in a local trace captured with Chromium
cr0x@server:~$ chromium --headless --disable-gpu --trace-startup --trace-startup-file=./trace.json https://www.example.com/
[0204/090312.112233:INFO:headless_shell.cc(661)] Written trace file to ./trace.json
cr0x@server:~$ jq '[.. | objects | select(has("dur") and has("name")) | select(.dur > 50000) | {name, dur, cat}] | sort_by(.dur) | reverse | .[0:5]' trace.json
[
{
"name": "EvaluateScript",
"dur": 182334,
"cat": "devtools.timeline"
},
{
"name": "FunctionCall",
"dur": 93422,
"cat": "devtools.timeline"
}
]
Ce que ça signifie : Vous avez des tâches > 50 ms, surtout l’évaluation de scripts. C’est du domaine INP.
Décision : Réduisez le JS envoyé/exécuté tôt. Splitez les bundles, retirez le code mort, et retardez les scripts tiers non critiques.
Task 8: Quantify JS/CSS bytes by route using build artifacts
cr0x@server:~$ ls -lh dist/assets | egrep '\.js$|\.css$' | head
-rw-r--r-- 1 cr0x cr0x 912K Feb 4 09:01 vendor-9a12c.js
-rw-r--r-- 1 cr0x cr0x 286K Feb 4 09:01 app-1b22f.js
-rw-r--r-- 1 cr0x cr0x 181K Feb 4 09:01 app-4aa2.css
Ce que ça signifie : Le chunk vendor est énorme. Ça se corrèle souvent avec le temps de parse/compile sur des téléphones milieu de gamme.
Décision : Auditez les dépendances. Si vous envoyez trois bibliothèques de dates et deux gestionnaires d’état, choisissez-en une et supprimez le reste.
Task 9: Detect unused CSS in a page (quick-and-dirty with coverage in Puppeteer)
cr0x@server:~$ node -e '
const puppeteer=require("puppeteer");
(async()=>{
const b=await puppeteer.launch({headless:"new"});
const p=await b.newPage();
await p.coverage.startCSSCoverage();
await p.goto("https://www.example.com/",{waitUntil:"networkidle2"});
const cov=await p.coverage.stopCSSCoverage();
let used=0,total=0;
for (const c of cov){ total+=c.text.length; used+=c.ranges.reduce((s,r)=>s+(r.end-r.start),0); }
console.log(`css_used=${(used/1024).toFixed(1)}KB css_total=${(total/1024).toFixed(1)}KB used_pct=${(used/total*100).toFixed(1)}%`);
await b.close();
})();'
css_used=28.4KB css_total=412.7KB used_pct=6.9%
Ce que ça signifie : Vous envoyez un manteau d’hiver à la plage. 93 % du CSS est inutilisé pour cette route.
Décision : Séparez le CSS par route, purgez les styles inutilisés, et évitez les frameworks globaux chargés partout « au cas où ».
Task 10: Verify font loading behavior and whether it risks CLS
cr0x@server:~$ curl -s -I https://www.example.com/assets/fonts/brand.woff2 | egrep -i 'content-type|cache-control|timing-allow-origin'
content-type: font/woff2
cache-control: public, max-age=31536000, immutable
timing-allow-origin: *
Ce que ça signifie : La police est cache-friendly et expose les timings. Bonne hygiène.
Décision : Si le CLS reste élevé, vérifiez l’absence de font-display et les incompatibilités des métriques de fallback ; envisagez une stack de polices système pour le texte critique.
Task 11: Find third-party script bloat in requests
cr0x@server:~$ jq -r '.log.entries[].request.url' page.har | egrep -i 'goog|doubleclick|segment|mixpanel|hotjar|optimizely|datadog|newrelic' | sort | uniq | head
https://cdn.segment.com/analytics.js/v1/...
https://www.googletagmanager.com/gtm.js?id=GTM-...
Ce que ça signifie : Vous avez des dépendances tierces qui peuvent s’exécuter tôt et souvent.
Décision : Placez-les derrière le consentement et/ou après le LCP. Si le business insiste, au moins rendez-les non bloquants et retardez leur initialisation.
Task 12: Check server-side compression and HTML size
cr0x@server:~$ curl -s -H 'Accept-Encoding: gzip, br' -I https://www.example.com/ | egrep -i 'content-encoding|content-type|content-length'
content-type: text/html; charset=utf-8
content-encoding: br
content-length: 24132
Ce que ça signifie : Brotli est activé et le HTML est ~24 KB compressé. C’est correct.
Décision : Si vous ne voyez pas de compression, activez-la à l’edge/origin ; si le HTML est énorme, arrêtez d’inliner des dumps JSON d’état dans la page.
Task 13: Validate that your CDN is serving correct image variants by Accept header
cr0x@server:~$ curl -s -I -H 'Accept: image/avif,image/webp,image/*,*/*;q=0.8' https://www.example.com/images/hero | egrep -i 'content-type|vary|cache-control'
content-type: image/avif
vary: Accept
cache-control: public, max-age=31536000, immutable
Ce que ça signifie : Vous servez de l’AVIF quand le client le supporte, et vous variez correctement sur Accept.
Décision : Si vous servez toujours JPEG/PNG, vous payez une taxe de bande passante qui impacte directement le LCP sur mobile.
Task 14: Check for accidental no-cache on static assets
cr0x@server:~$ curl -s -I https://www.example.com/assets/app-1b22f.js | egrep -i 'cache-control|etag'
cache-control: public, max-age=31536000, immutable
etag: "a1b2c3"
Ce que ça signifie : Bien : assets fingerprintés mis en cache à long terme.
Décision : Si vous voyez no-cache sur des assets fingerprintés, corrigez-le immédiatement ; vous forcez des téléchargements répétés et brûlez de l’INP via parse supplémentaire.
Trois mini-récits d’entreprise issus des tranchées de la perf
Mini-récit 1 : L’incident causé par une mauvaise hypothèse (« CDN signifie rapide »)
Une équipe d’app grand public a déployé une fonctionnalité de personnalisation sur la page d’entrée. C’était sobre : « Bienvenue », plus quelques éléments recommandés. Ils avaient déjà un CDN, donc ils ont supposé que l’impact serait négligeable. Le changement a passé les tests unitaires, les e2e, et la vérification synthétique de perf sur une connexion de bureau rapide.
Deux jours plus tard, le rapport CWV mobile a plongé. Les tickets de support décrivaient une « page blanche » et « le tap ne répond pas ». Le produit a supposé une régression JavaScript. L’équipe frontend a commencé à réduire la taille des bundles. L’équipe backend a commencé à profiler les APIs. Tout le monde était occupé ; personne n’était efficace.
Le SRE en astreinte a fait la chose ennuyeuse : curl -I sur la page d’accueil et a regardé les en-têtes. Le HTML était maintenant Cache-Control: private, no-store, plus Vary: Cookie. Le code de personnalisation touchait l’état de session tôt, ce qui a fait que le framework a marqué la réponse entière comme non cacheable. Le CDN n’était pas « lent » ; il était contourné. Chaque requête frappait l’origine, faisait du SSR, et la longue traîne des utilisateurs mobiles payait le prix plein.
La correction n’a pas été héroïque. Ils ont séparé la page en une shell cacheable et un petit bloc personnalisé récupéré par le client après le premier paint. Ils ont aussi réduit les cookies, car les en-têtes de requête grossissaient assez pour avoir un impact sur les réseaux contraints.
La leçon est claire : les hypothèses sur la mise en cache ne sont pas de l’architecture. Les en-têtes sont de l’architecture. Vérifiez le comportement du cache avec des requêtes réelles, pas avec des impressions.
Mini-récit 2 : L’optimisation qui s’est retournée contre eux (lazy-loading de la hero)
Un tableau de bord B2B avait de bons CWV sur desktop mais un LCP mobile médiocre sur les pages marketing. L’équipe a décidé de « lazy-loader plus d’images » parce que c’était un gain facile et le web regorge de conseils qui semblent écrits par quelqu’un qui n’a jamais livré un site avec une bannière hero.
Ils ont mis loading="lazy" sur toutes les images globalement via un wrapper de composant. Ça semblait correct en dev local. En staging, les tests synthétiques montraient moins d’octets tôt. Le changement a été déployé.
En une semaine, le LCP s’est dégradé. L’élément LCP était l’image hero, et le navigateur la dépriorisait désormais. La requête d’image commençait plus tard, le décodage plus tard, et le contenu principal arrivait plus tard en termes perçus par l’utilisateur. La conversion a chuté légèrement, pas assez pour déclencher des alarmes, mais assez pour que le marketing organise des réunions « le site semble lent ».
Quand ils ont enfin tracé le problème, c’était évident : l’image hero avait été retardée par conception. Ils ont retiré le lazy-loading pour les images au-dessus de la ligne, ajouté des sources responsives, et utilisé les hints de preload de manière sélective. L’effet net a été moins d’octets et un paint plus précoce — parce qu’ils ont priorisé les bons octets.
La leçon : le navigateur est bon pour prioriser quand vous ne lui mentez pas. Le lazy-load n’est pas une vertu ; c’est un outil. Servez-vous en là où il appartient.
Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise (budgets et canaries)
Une fintech avait une culture de la performance peu glamour. Ils avaient des budgets par route : JS/CSS max, et une règle « pas de nouveaux scripts tiers sans revue ». Les ingénieurs râlaient parfois. Puis ils ont oublié, ce qui est le plus grand compliment pour un mécanisme de contrôle.
Un trimestre, un nouveau fournisseur d’analytics a été introduit. Le snippet du fournisseur était petit, mais il faisait charger une bibliothèque plus grosse et commençait à faire du travail lourd sur les interactions utilisateur. En staging, personne n’a remarqué ; le trafic et les appareils de staging n’étaient pas représentatifs, et l’app était déjà « assez rapide » en tests labo.
La pipeline de déploiement a lancé un canary en production avec gating RUM. Le canary a montré une régression d’INP concentrée sur des appareils Android milieu de gamme et sur la route de checkout. Le rollout s’est pausé automatiquement. Pas de drame, pas de blâme. L’équipe avait un diff net : « INP up ; nouveau script tiers chargé avant l’interaction. »
Ils ont travaillé l’intégration avec le fournisseur : initialisation retardée jusqu’à ce que la page soit idle, et limitation du tracking aux routes où il importait réellement. Ils l’ont aussi sandboxé derrière le consentement. Le canary est passé ; le déploiement a repris.
La leçon : des garde-fous ennuyeux valent mieux que des postmortems héroïques. Les budgets et les canaries ne sont pas du « process ». Ce sont un système qui vous empêche d’envoyer de la latence à vos clients.
Erreurs courantes : symptôme → cause racine → correctif
1) Symptom : LCP mauvais seulement à la première visite
Cause racine : Les assets sont bien mis en cache, mais l’image hero critique est trop grosse ou pas servie dans des formats modernes ; les visiteurs de première visite subissent le coût complet de téléchargement/décodage.
Fix : Utilisez des images responsives et AVIF/WebP ; assurez-vous que la hero est demandée tôt ; vérifiez les en-têtes de cache et la compression CDN quand applicable.
2) Symptom : LCP mauvais et TTFB aussi élevé
Cause racine : Latence d’origine, SSR lent, contournement du cache via personnalisation ou cookies, ou CDN mal configuré.
Fix : Rendez le HTML cacheable ; déplacez la personnalisation vers des includes edge ou un fetch client après le paint ; profilez le SSR ; corrigez les clés de cache ; réduisez Vary et la taille des cookies.
3) Symptom : CLS pique sur des pages avec pubs ou bannières de consentement
Cause racine : Insertion DOM tardive au-dessus de la ligne, slots d’annonce qui redimensionnent après le chargement, bannière qui pousse le contenu vers le bas.
Fix : Réservez de l’espace (conteneurs fixes ou min-height), affichez des placeholders, évitez d’insérer des changements de layout au-dessus de la ligne après le premier paint.
4) Symptom : CLS petit en labo mais élevé en RUM
Cause racine : Variabilité réel-utilisateur : différentes tailles de viewport provoquent des wrapping différents ; les polices swap différemment ; les widgets tiers se comportent de façon incohérente.
Fix : Utilisez le RUM pour identifier les sources de shift ; testez avec des viewports variés ; appliquez des dimensions explicites et des fallback de polices stables ; contraignez les conteneurs tiers.
5) Symptom : INP mauvais sur des pages « simples »
Cause racine : Scripts tiers ou listeners globaux qui font du travail sur chaque interaction ; hydratation lourde ; tâches longues du runtime du framework.
Fix : Retardez l’initialisation des tiers ; retirez les listeners inutiles ; segmentez l’hydratation ; déplacez le travail non critique vers des idle callbacks ; réduisez le JS envoyé.
6) Symptom : Réduire la taille du bundle n’a pas amélioré l’INP
Cause racine : Le problème n’est pas le téléchargement ; ce sont les patterns d’exécution (re-renders, thrash de layout, handlers lourds) ou un chemin d’interaction unique coûteux.
Fix : Tracez les interactions ; trouvez les tâches longues ; corrigez les tempêtes de re-render ; réduisez le travail synchrone dans les handlers ; utilisez le batching et la mémoïsation judicieusement.
7) Symptom : LCP régresse après avoir ajouté « preload everything »
Cause racine : Inversion de priorité : preloader trop de ressources entre en concurrence avec la vraie ressource LCP.
Fix : Preloadez seulement les vraies ressources critiques (généralement une hero, éventuellement une police). Validez avec la priorité réseau dans les traces.
8) Symptom : Le mobile est bien pire que le desktop sur toutes les métriques
Cause racine : JS lié au CPU et travail de layout lourd ; le desktop masque ça par la force brute.
Fix : Testez avec throttling CPU et profils d’appareil milieu de gamme ; réduisez l’exécution JS ; cassez les tâches longues ; simplifiez l’UI et le DOM.
Listes de contrôle / plan étape par étape
Plan étape par étape pour un sauvetage LCP (un template à la fois)
- Identifier l’élément LCP depuis le RUM (ou la trace) pour ce template. Si vous ne pouvez pas le nommer, arrêtez-vous et trouvez-le.
- Vérifier le TTFB sur hit et miss de cache. Si le hit cache est lent, corrigez edge/origin avant le travail frontend.
- Auditer les octets hero : format, dimensions, compression et en-têtes de cache.
- Assurer la priorité : évitez le lazy-load au-dessus de la ligne ; preload la hero quand approprié ; ne la noyez pas sous d’autres preloads.
- Réduire le blocage du rendu : inline/split du CSS critique ; defer le JS non critique.
- Re-mesurer avec le RUM au p75 sur mobile. Déployez, puis vérifiez. Ne vous arrêtez pas à « le labo est mieux ».
Plan étape par étape pour stabiliser le CLS
- Lister les éléments qui shiftent le plus à l’aide du RUM ou des « Layout Shift Regions » de DevTools.
- Réserver de l’espace pour images/iframes/ads avec width/height ou
aspect-ratio. - Arrêter les insertions tardives au-dessus de la ligne ou les rendre dans un emplacement réservé dès le départ.
- Corriger les polices : assurer des métriques de fallback sensées ; utiliser une stratégie
font-displayqui n’entraîne pas de reflow surprenant. - Retester sur plusieurs viewports car les différences de wrapping peuvent créer des « CLS seulement sur certains écrans ».
Plan étape par étape pour des améliorations INP durables
- Tracer une interaction problématique (click/input) sur un profil milieu de gamme et trouver les tâches les plus longues.
- Identifier le propriétaire : votre code vs tiers vs runtime du framework.
- Casser les tâches longues et déplacer le travail hors du chemin critique d’interaction.
- Réduire les rerenders : éviter les mises à jour d’état qui déclenchent de larges arbres de composants ; virtualisez les grandes listes.
- Déléguer les scripts non critiques et arrêter les travaux analytiques synchrones sur les événements d’entrée.
- Imposer un budget sur les tâches longues et sur la taille des bundles par route, puis l’appliquer en CI/canary.
Checklist opérationnelle : conserver les gains et éviter les régressions
- Dashboards RUM par template et classe d’appareil, alertant sur les régressions p75.
- Budgets de performance pour octets JS/CSS et ajouts tiers.
- Déploiements canary avec rollback/pause automatique en cas de régression CWV.
- Nettoyage régulier des dépendances (trimestriel OK ; hebdomadaire est fantaisie).
- Tests des en-têtes de cache en CI pour les routes et assets critiques.
FAQ
1) Dois-je optimiser les scores labo ou le RUM ?
RUM pour la vérité, labo pour le débogage. Les tests labo sont contrôlés ; ils sont excellents pour détecter des régressions évidentes et isoler des causes. Le RUM vous dit ce que vivent réellement les clients, y compris appareils lents et réseaux bizarres.
2) Pourquoi mon score Lighthouse change à chaque exécution ?
Parce que la performance est un système distribué : jitter réseau, ordonnancement CPU, état du cache et comportement tiers varient. Faites plusieurs exécutions, regardez les distributions, et concentrez-vous sur les goulets répétables comme les images hero énormes, le CSS bloquant et les tâches longues.
3) Une SPA est-elle intrinsèquement pire pour les CWV ?
Pas intrinsèquement, mais il est plus facile de se tromper. Les SPA retardent souvent le contenu signifiant jusqu’à l’exécution du JS, ce qui nuit au LCP et peut nuire à l’INP. SSR/streaming et hydratation sélective peuvent combler l’écart, mais seulement si vous gardez le chemin critique léger.
4) Dois-je inliner tout le CSS ?
Non. Inlinez le CSS critique pour au-dessus de la ligne, séparez le reste, et évitez d’envoyer tout le design system sur chaque route. Inliner tout peut gonfler le HTML et retarder le premier octet sur les connexions lentes.
5) Les preloads sont-ils toujours bons ?
Non. Preloadez la ou les une à deux ressources qui définissent vraiment le LCP (souvent une image hero et peut-être une police). Le sur-preloading entre en concurrence pour la bande passante et peut retarder la vraie ressource critique.
6) Comment les polices affectent-elles les CWV ?
Les polices peuvent retarder le rendu du texte (nuire à la perception du chargement) et provoquer des décalages quand elles sont swapées (nuire au CLS). Cachez-les agressivement, limitez les variantes, et assurez des fallbacks qui n’entraînent pas de grands reflows.
7) Quel est le « gros gain » le plus rapide pour le CLS ?
Donnez des dimensions à tout. Images, iframes, slots d’annonces. Réservez de l’espace pour les bannières. Les corrections CLS rapides ressemblent à du ménage parce que c’en est.
8) Quel est le « gros gain » le plus rapide pour l’INP ?
Retirez ou retardez les scripts tiers qui s’exécutent sur interaction et casse les longues tâches. Aussi : stoppez le travail lourd synchronisé dans les handlers d’entrée. Si vous devez faire de l’analytics, tamponnez-le.
9) Pourquoi le mobile est-il disproportionnellement mauvais ?
Parce que les CPU et radios mobiles sont plus lents, et la pression mémoire change tout. Le desktop masque beaucoup de péchés. Si vous ne testez que sur un laptop dev, vous benchmarkez la mauvaise machine.
10) Si mon backend est rapide, puis-je ignorer le TTFB ?
Non. Le TTFB inclut le réseau, TLS, le comportement CDN, le routage edge et les ratés de cache. La latence backend peut être correcte tandis que les utilisateurs attendent toujours parce que vous avez accidentellement rendu le HTML non cacheable ou mal routé le trafic.
Prochaines étapes à livrer cette semaine
Ne commencez pas par « optimiser tout ». Commencez par un template qui échoue et obtenez un gain mesurable au p75 mobile.
- Choisissez le pire template à fort trafic depuis le RUM (pas la page d’accueil par tradition).
- Appliquez la feuille de route rapide de diagnostic et identifiez si le TTFB, le blocage de rendu, les octets hero ou les tâches longues dominent.
- Livrez un correctif LCP : rendez le HTML cacheable, priorisez la hero, ou réduisez les octets hero. Vérifiez via les en-têtes et le RUM.
- Livrez un correctif CLS : réservez de l’espace pour la source de shift principale. Vérifiez que le CLS baisse dans le RUM.
- Livrez un correctif INP : retirez/déléguez un script tiers ou cassez une voie de tâche longue. Vérifiez l’amélioration de la latence d’interaction.
- Ajoutez des garde-fous : budgets, canary gating, et une revue hebdomadaire « qu’est-ce qu’on a ajouté ? » des dépendances.
Le secret honnête : le meilleur travail sur les CWV ressemble à du travail de fiabilité. Mesurez, isolez, corrigez, vérifiez et empêchez les régressions. Si vous faites des astuces intelligentes sans courbe avant/après, vous ne peaufinez pas la performance — vous collectionnez des charmes.