10 mars 2026 · Tommy Bordas

Audit de performance web, ma méthode en 5 étapes et les quick wins systématiques

performanceauditcore-web-vitalsquick-winsseo

Un audit de performance web efficace n'est pas un score Lighthouse. C'est une méthode en 5 étapes : mesurer le réel, profiler, prioriser par impact, appliquer les quick wins, puis monitorer les Core Web Vitals. La méthode, étape par étape.

Pourquoi une méthode, pas un score Lighthouse

Lighthouse donne un score en lab, sur une machine puissante, réseau filaire, sans cache froid ni extensions. Vos utilisateurs, eux, sont sur un mobile milieu de gamme, en 4G, avec un CPU trois à quatre fois plus lent et une batterie qui throttle. Deux mondes. Un audit sérieux part toujours des données terrain (field data) avant de toucher au code : c'est ce que mesure Google pour le ranking, via le rapport CrUX (Chrome User Experience Report).

La différence n'est pas cosmétique. Une page peut afficher 95 en lab et offrir un LCP terrain à 4,5 s au 75e centile, exactement le seuil qui vous classe « à améliorer » dans la Search Console. Le score est un indice de diagnostic ; la donnée terrain est la vérité.

Critère Lab (synthétique) Terrain (field / RUM)
Source Lighthouse, WebPageTest, PageSpeed CrUX, librairie web-vitals, RUM maison
Conditions Contrôlées, reproductibles Vrais appareils, vrais réseaux
Métrique INP Estimée / simulée Mesurée sur interactions réelles
Idéal pour Déboguer, comparer un avant/après Décider quoi prioriser, suivre le ranking
Limite Ne voit pas vos vrais users Bruitée, demande du volume

Rappel des seuils Core Web Vitals (« bon » au 75e centile) : LCP ≤ 2,5 s, INP ≤ 200 ms, CLS ≤ 0,1. INP a remplacé FID en mars 2024 ; si votre suivi parle encore de FID, il est périmé.

À retenir : optimisez ce que vivent vos utilisateurs, pas ce que voit votre machine de dev. Le score Lighthouse est un point de départ pour déboguer, jamais l'objectif final.

Étape 1 : Mesurer le réel (web-vitals + sendBeacon)

On collecte les Core Web Vitals sur le terrain avec la librairie officielle web-vitals, et on les envoie à un endpoint avec sendBeacon (qui survit à la fermeture de l'onglet, contrairement à un fetch classique). Point clé : pour LCP, CLS et INP, la valeur n'est définitive qu'au déchargement de la page. On rapporte donc dans le callback, et on ne tente pas de lire la métrique « tout de suite ».

import { onLCP, onINP, onCLS, onTTFB, onFCP } from 'web-vitals';

const report = (metric) => {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,        // 'good' | 'needs-improvement' | 'poor'
    id: metric.id,
    navigationType: metric.navigationType,
    path: location.pathname,
  });
  // sendBeacon survit à la navigation ; fallback fetch keepalive
  (navigator.sendBeacon && navigator.sendBeacon('/vitals', body)) ||
    fetch('/vitals', { body, method: 'POST', keepalive: true });
};

onLCP(report);
onINP(report);
onCLS(report);
onTTFB(report);
onFCP(report);

Quelques règles que je m'impose ici :

  • Segmenter par page et par device. Une moyenne globale cache tout. Le LCP de la home n'a rien à voir avec celui d'une fiche produit.
  • Raisonner au 75e centile, jamais en moyenne. C'est la métrique de Google, et une moyenne se fait écraser par les bons cas.
  • Attribuer. web-vitals/attribution vous dit quel élément est le LCP ou quelle interaction a produit le pire INP. C'est ce qui transforme une mesure en piste d'action.

Pas de volume de trafic suffisant pour du RUM maison ? On démarre sur les données CrUX (via PageSpeed Insights ou l'API CrUX) et on bascule vers du RUM dès que le projet le justifie.

Étape 2 : Profiler pour trouver les goulots

Mesurer dit quoi ; profiler dit pourquoi. On ouvre l'onglet Performance des DevTools, on active le throttling CPU 4× slowdown et le réseau « Slow 4G », puis on enregistre un chargement à froid (cache vidé, mode navigation privée pour neutraliser les extensions). On cherche les trois suspects habituels.

Symptôme Cause fréquente Où le voir dans les DevTools
LCP lent Image hero non optimisée, CSS/JS bloquant, TTFB serveur élevé Section Timings + LCP dans Performance, Network waterfall
INP élevé Long tasks JS, gros gestionnaires d'événements, hydration tierce Main thread, blocs rouges « Long Task » (> 50 ms)
CLS visible Images/iframes sans dimensions, fonts (FOIT/FOUT), bannières injectées Overlay Layout Shift Regions, calque Experience
TTFB lent Backend lent, pas de cache, redirections en cascade Première barre du waterfall Network

Les repères que je traque concrètement :

  • Long tasks : toute tâche du thread principal au-delà de 50 ms bloque les interactions. Les blocs au liseré rouge dans la timeline sont vos premiers candidats au découpage (yield, requestIdleCallback, web workers).
  • Ressources bloquantes : un <script> synchrone dans le <head> ou une feuille CSS volumineuse retardent le rendu. Le waterfall réseau montre exactement ce qui retient le premier paint.
  • Layout shifts : l'overlay « Layout Shift Regions » (menu ⋮ → More tools → Rendering) fait clignoter en bleu les zones qui sautent. Neuf fois sur dix : une image sans width/height ou une web font qui repousse le texte.

À retenir : ne profilez jamais sur votre machine en conditions nominales. CPU 4×, Slow 4G, cache vidé : sinon vous optimisez un problème que vos utilisateurs n'ont pas, et vous ratez celui qu'ils ont.

Étape 3 : Prioriser par impact, pas par facilité

Toutes les optimisations ne se valent pas. Je classe chaque piste sur une matrice impact × effort et j'attaque le quadrant « fort impact / faible effort » en premier. C'est ce qui fait qu'un audit livre des gains en jours, pas en trimestres.

Impact ↑
  │  Gros chantiers     │  À FAIRE         │
  │  (à planifier :     │  d'abord         │
  │   refonte SSR,      │  (quick wins :   │
  │   archi images)     │   AVIF, preload) │
  ├─────────────────────┼──────────────────┤
  │  À ignorer          │  Bonus si temps  │
  │  (micro-gains)      │  (polish)        │
  └─────────────────────┴──────────────────→ Effort

Pour décider, je relie chaque piste à la métrique qu'elle déplace : une image hero en AVIF + fetchpriority agit sur le LCP ; découper un long task ou différer un script tiers agit sur l'INP ; dimensionner les médias agit sur le CLS. Si une optimisation ne touche aucun Core Web Vital ni le TTFB, elle descend dans la pile.

Étape 4 : Les quick wins systématiques

Ces corrections reviennent sur presque chaque audit et offrent le meilleur ratio impact/effort. Je les applique dans cet ordre.

Images : le premier levier du LCP

L'image hero est le LCP de la majorité des pages. On la sert en AVIF (ou WebP en fallback), avec width/height explicites (anti-CLS), fetchpriority="high" pour qu'elle parte avant tout le reste, et surtout pas de loading="lazy" dessus. Le lazy-load, lui, est réservé à tout ce qui est sous la ligne de flottaison.

<!-- Image LCP : prioritaire, dimensionnée, AVIF + fallback -->
<picture>
  <source srcset="/img/hero.avif" type="image/avif">
  <source srcset="/img/hero.webp" type="image/webp">
  <img src="/img/hero.jpg" width="1200" height="600"
       fetchpriority="high" decoding="async"
       alt="Description utile pour l'accessibilité et le SEO">
</picture>

<!-- Image hors viewport : différée -->
<img src="/img/bloc-3.webp" width="800" height="500"
     loading="lazy" decoding="async" alt="…">

Polices : tuer le FOIT et stabiliser la mise en page

<!-- Précharger la police critique (la seule du premier rendu) -->
<link rel="preload" href="/fonts/inter-subset.woff2" as="font"
      type="font/woff2" crossorigin>
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-subset.woff2') format('woff2');
  font-display: swap;     /* le texte s'affiche tout de suite */
  font-weight: 400 700;   /* une variable font couvre plusieurs graisses */
}

Trois gestes : font-display: swap (le texte s'affiche en police système puis bascule, fin du FOIT) ; preload de la seule police du premier rendu ; subsetting pour ne charger que les glyphes utilisés (latin-1 au lieu de l'alphabet entier divise souvent le poids par 5). Bonus anti-CLS : aligner size-adjust/ascent-override de la police de secours sur la web font pour éviter le saut de texte au swap.

JavaScript : moins, plus tard, à la demande

  • defer (ou type="module") sur tout le JS non critique : il n'empêche plus le rendu.
  • Code mort : un build avec tree-shaking + une passe de coverage dans les DevTools révèlent les Ko jamais exécutés. Sur du WordPress, ça veut souvent dire désactiver les scripts d'un plugin sur les pages où il ne sert pas.
  • Tiers à l'interaction : chat, cartes, players, widgets sociaux… on les charge au premier scroll, clic ou survol, pas au load. Le pattern « facade » (une image cliquable qui hydrate le vrai widget à la demande) est imbattable pour les embeds YouTube/Maps.

Cache, compression, CDN : le gain serveur

  • Brotli (ou Gzip à défaut) sur le HTML/CSS/JS : -15 à -25 % de poids transféré vs non compressé.
  • Cache-Control: public, max-age=31536000, immutable sur les assets versionnés (hash dans le nom de fichier), pour qu'un visiteur récurrent ne retélécharge rien.
  • CDN pour rapprocher les octets de l'utilisateur et écraser le TTFB des audiences éloignées.

Étape 5 : Monitorer dans la durée (budget de perf en CI)

Une optimisation qui n'est pas surveillée régresse. Un plugin ajouté, une image oubliée, un script marketing collé en prod, et le LCP repart. On pose un budget de performance dans la CI et on bloque toute PR qui le dépasse. Avec Lighthouse CI, ça tient en un fichier.

{
  "ci": {
    "assert": {
      "assertions": {
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "interaction-to-next-paint": ["error", { "maxNumericValue": 200 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "total-byte-weight": ["warn", { "maxNumericValue": 1600000 }],
        "unused-javascript": ["warn", { "maxNumericValue": 100000 }]
      }
    }
  }
}

En parallèle, on garde un œil sur la donnée terrain (Search Console → Signaux web essentiels, ou un dashboard alimenté par le endpoint /vitals de l'étape 1). Le lab attrape les régressions avant le merge ; le terrain confirme que les gains tiennent chez les vrais utilisateurs.

La checklist d'audit complète

Mesure & diagnostic

  • Collecter les Core Web Vitals terrain (web-vitals + sendBeacon, ou CrUX)
  • Raisonner au 75e centile, segmenté par page et par device
  • Profiler à froid en conditions mobiles réalistes (CPU 4×, Slow 4G, cache vidé)
  • Identifier long tasks, ressources bloquantes et layout shifts
  • Classer chaque piste sur la matrice impact × effort

Quick wins

  • Image LCP en AVIF/WebP, dimensionnée, fetchpriority="high", jamais lazy
  • loading="lazy" sur tous les médias hors viewport
  • Polices : font-display: swap, preload de la critique, subsetting
  • JS : defer, suppression du code mort, tiers chargés à l'interaction
  • Brotli + Cache-Control immutable sur les assets versionnés + CDN

Pérennité

  • Budget de performance dans la CI (Lighthouse CI) qui bloque les régressions
  • Suivi continu de la donnée terrain (Search Console / dashboard RUM)
  • Re-mesurer et comparer l'avant/après sur chaque déploiement

Pour aller plus loin

Pour un cas d'application concret côté front, avec chiffres à l'appui, voir : Comment j'ai optimisé les performances d'une application Angular de 40 %.

Vous voulez un audit de performance web complet (données terrain, profil DevTools, roadmap priorisée sur la matrice impact × effort et quick wins activables tout de suite) ? C'est exactement le travail que je mène sur chaque mission, WordPress/WooCommerce comme Angular. Parlons de votre site et de vos Core Web Vitals.