Aller au contenu principal
Tous les articles
Performance

Traquer les 50 ko qui ruinent votre LCP : retour de chantier

50 ko de trop sur mobile 4G, c'est 200 ms de LCP envolées. Voici comment on les a retrouvés et supprimés, étape par étape.

Équipe Performance VALRY LABS12 mai 202611 min de lecture

Le décor : un marketing site Next.js 15 sous budget LCP

Le chantier commence sur un site marketing classique : une quinzaine de pages, un blog sous MDX, une équipe produit satisfaite du rendu visuel. Côté stack, Next.js 15 en App Router, déployé sur Vercel, images servies via next/image, fontes via next/font. Sur le papier, tout est en ordre. Le budget interne est clair : LCP inférieur à 1,5 seconde sur mobile 4G (Motorola Moto G Power, profil Lighthouse mobile, throttling 4x CPU). Or le rapport Lighthouse mobile affiche 2,1 s en p75 simulé. Soit 600 ms au-dessus du budget, et un Core Web Vitals qui passe de "Good" à "Needs Improvement" sur la page d'accueil.

Le premier réflexe à ce stade, c'est de ne pas ouvrir un terminal et de crier sur les dépendances. On ouvre trois onglets : le rapport Lighthouse, le field data CrUX (quand il est disponible) et la liste des éléments LCP identifiés par Lighthouse. Dans 80 % de nos chantiers marketing, le coupable n'est pas unique : c'est une accumulation de 5 à 10 micro-décisions techniques qui, additionnées, dépassent le budget. Ici, on cherche un delta de 600 ms, soit environ 50 à 80 ko de ressources bloquantes ou mal priorisées. C'est parti pour la chasse aux octets.

La méthode de diagnostic : trois outils, dans cet ordre

Avant de toucher au code, on déroule toujours la même trame. Étape 1 : Chrome DevTools, onglet Performance Insights (pas l'ancien Performance, qui est trop bas niveau pour ce stade). On recharge la page en mode throttling CPU 4x + réseau Fast 3G, on observe le curseur LCP, et on note les blocs qui passent du gris (idle) au rouge (main thread bloqué). Étape 2 : WebPageTest, on lance un run depuis un Moto G Power réel (Dulles, Virginie), on récupère le filmstrip image par image : à quelle seconde le hero devient-il visible ? Est-ce que la police s'affiche avant ou après l'image LCP ?

Étape 3 : Lighthouse, on ouvre la vue "Treemap" du rapport (View Treemap). C'est l'outil le plus parlant pour un public non technique : chaque rectangle représente un chunk JS, sa taille est proportionnelle aux octets transférés, et la couleur indique le ratio code utilisé / code inutilisé au chargement. Sur ce chantier, le treemap affichait trois blocs rouges flagrants : un vendor.js à 47 % unused, un fonts.css à 60 % unused, et un chunk framer-motion à 0 % utilisé sur la page d'accueil. Enfin, l'onglet Sources > Coverage confirme : on a 312 ko de JS chargés, dont 178 ko inutilisés au premier rendu. Le diagnostic est posé en moins de 20 minutes.

Les 7 suspects habituels (et lequel a vraiment fauté)

Sur les chantiers marketing Next.js, on retrouve presque toujours les mêmes profils de gaspillage. Suspect n°1 : les icônes SVG importées comme composants React. Le projet utilisait lucide-react avec un `import { Icon } from "lucide-react"` mais aussi une bibliothèque maison de 30 SVG inline (1,2 ko chacun), tous inclus dans le bundle car résolus à la compilation via un barrel file index.ts. Résultat : 36 ko d'icônes dont 28 n'étaient même pas référencées sur la page d'accueil.

Suspect n°2 : date-fns et ses locales. Un `import { fr } from "date-fns/locale"` avait glissé dans un composant de date de publication. Sauf que date-fns v3 importe l'intégralité de la locale FR (jours, mois, formats localisés, formats relatifs) : 15 ko pour un simple `format(date, "d MMMM yyyy")`. Suspect n°3 : Framer Motion. L'agence avait chargé la bibliothèque (45 ko gzippé) pour une seule animation de hero, un fade-in de 200 ms. La même animation en CSS pur fait 0 ko. Suspect n°4 : l'image hero pilotée par le CMS, avec un attribut `sizes="100vw"` au lieu de `sizes="(max-width: 768px) 100vw, 50vw"`. next/image servait donc une variante 2400 px de large à un viewport mobile de 360 px.

Suspect n°5 : la fonte webfont non sous-ensemble. Le projet utilisait une fonte éditoriale (200 ko WOFF2) déclarée via next/font, mais sans subset latin : tous les glyphes cyrilliques, grecs, et symboles mathématiques étaient embarqués. Suspect n°6 : le bandeau de consentement (Axeptio) chargé en synchrone dans le `<head>`, avec un script de 15 ko qui bloquait le parseur pendant 120 ms. Suspect n°7 : GTM (Google Tag Manager) avec un container de 14 tags déclenchés sur PageView, dont seulement 3 étaient réellement nécessaires (GA4, Hotjar, Meta Pixel). Les 11 autres (LinkedIn Insight, Pinterest, Quora, TikTok, etc.) étaient des résidus de campagnes passées.

Sur ce chantier, les vrais coupables du delta de 600 ms étaient les suspects n°4 (image), n°5 (fonte) et n°6 (consent). Mais tous les suspects méritaient un nettoyage : on a traité les sept.

La séquence de correction, par ordre d'impact

On attaque toujours par le plus gros gain, pas par le plus facile. Ordre d'impact réel sur ce chantier : (1) Subset des fontes via next/font avec `subsets: ["latin"]` et `display: "swap"` : -90 ko de WOFF2 transférés. La fonte éditoriale passe de 200 ko à 28 ko. (2) Tree-shaking des icônes : remplacement du barrel file par des imports nommés explicites `import { ChevronRight } from "lucide-react"`, et suppression de la bibliothèque maison au profit de lucide-react déjà chargée. Gain bundle : -30 ko.

(3) Chargement asynchrone de GTM : passage de `<script src="gtm.js">` à `async`, et audit des 14 tags pour n'en garder 3. Gain sur le chemin bloquant : -25 ko, soit ~80 ms de TBT en moins. (4) Correction de l'attribut `sizes` du hero : `sizes="(max-width: 768px) 92vw, (max-width: 1536px) 50vw, 33vw"`. next/image sert désormais une variante 640 px au lieu de 2400 px. Gain transfert : -180 ko par page vue mobile. (5) Déferrage du bandeau consent : `next/script` avec `strategy: "lazyOnload"`, et retrait du `<head>`. Gain : -15 ko sur le chemin bloquant.

Total du chantier : environ 340 ko de transferts en moins sur la page d'accueil mobile, dont 130 ko sur le chemin critique (premier rendu). Le LCP simulé Lighthouse passe de 2,1 s à 0,9 s. Le Total Blocking Time passe de 280 ms à 90 ms. Le CLS reste stable à 0,02. Tous les Core Web Vitals passent en vert.

La boucle de mesure : Lighthouse, CrUX, Sentry

Un chantier perf sans boucle de mesure, ce n'est pas un chantier perf : c'est une intuition. On déroule trois niveaux. Niveau 1 (lab data) : Lighthouse mobile avant/après, 5 runs consécutifs en navigation privée, on prend la médiane. Ici : LCP 2,1 s → 0,9 s, TBT 280 ms → 90 ms, FCP 1,4 s → 0,7 s. Niveau 2 (field data) : CrUX via PageSpeed Insights API, on compare les 28 jours précédents et les 28 jours suivants. Le p75 LCP réel passe de 2,4 s à 1,3 s. C'est cette métrique qui compte pour Google et pour les utilisateurs réels : elle seule déclenche le signal "Good URL".

Niveau 3 (monitoring continu) : on branche Sentry Performance sur le site pendant 7 jours, avec le SDK @sentry/nextjs. On observe les percentiles p50, p75, p90 du LCP sur les utilisateurs Android (segmentation par device). On vérifie qu'aucune régression n'apparaît sur les pages modifiées, et que les sessions "slow" (LCP > 2,5 s) chutent de 14 % à 2 %. Au bout de 7 jours sans alerte, le chantier est validé et le budget perf est mis à jour dans le fichier budget.json du repo : `LCP_MOBILE_MAX = 1500` ms, `JS_BUNDLE_MAX = 180` ko gzippé.

Les leçons qui se généralisent

Première leçon : sur mobile 4G mid-range (Moto G Power, Snapdragon 6 GHz), 50 ko de JS bloquant représentent environ 200 ms de LCP. Le ratio n'est pas linéaire : au-delà de 170 ko de JS total, le main thread sature et chaque ko supplémentaire coûte 5 à 8 ms. La courbe de Miller s'applique. Deuxième leçon : le treemap Lighthouse est l'outil de pédagogie le plus puissant pour expliquer la perf à un client non technique. Un rectangle rouge de 80 ko inutilisé parle plus qu'un discourse sur le tree-shaking.

Troisième leçon : le "code inutilisé" est le tueur silencieux de budget. Il ne plante rien, ne remonte aucune erreur Sentry, ne se voit pas en QA visuel. Mais il s'accumule : un import oublié ici, une dépendance de dev passée en prod là, un tag GTM résiduel d'une campagne 2023. La seule défense, c'est un budget.json versionné, un check CI via bundle-analyzer, et un audit trimestriel des dépendances. Quatrième leçon : les fontes sont presque toujours sous-optimisées. next/font gère le sous-ensemble latin par défaut, mais beaucoup de projets chargent des fontes éditoriales tierces sans `subsets` ni `unicode-range`. C'est un gain de 80 à 150 ko gratuit.

Dernière leçon, et la plus importante pour un studio : la perf n'est pas un livrable one-shot, c'est un contrat continu. Sur ce chantier, six semaines plus tard, une PR a réintroduit Framer Motion pour une animation de hover sur les cards. Le budget.json a déclenché un warning en CI, la PR a été ajustée. Sans cette garde-fou, on aurait reperdu 45 ko en deux mois. La discipline, c'est ce qui sépare un site qui reste rapide d'un site qui regraisse.

À retenir

Points clés.

  • 50 ko de JS bloquant sur mobile 4G mid-range = ~200 ms de LCP en plus. Chaque ko compte.
  • Lighthouse Treemap + Sources Coverage : 20 minutes suffisent pour poser un diagnostic clair.
  • Les 7 suspects récurrents : icônes en barrel file, locales date-fns, Framer Motion pour du CSS, images mal sizes, fontes non subset, consent synchrone, GTM surchargé.
  • Ordre d'impact typique : fontes > icônes > GTM > images > consent. On attaque par le gain, pas par la facilité.
  • Le code inutilisé ne remonte aucune erreur : c'est le tueur silencieux de budget perf.
  • Sans budget.json versionné et check CI, tout chantier perf se dégrade en 6 à 12 semaines.
LCPCore Web VitalsPerformanceNext.jsBundleOptimisation