← Retour au blog

Core Web Vitals : comment j'ai réduit le LCP de 4.2s à 0.8s

Techniques concrètes pour optimiser LCP, FID et CLS sans refondre votre app. Avec mesures avant/après et gains SEO réels.

Le déclic : -73% de trafic organique en 3 mois

En juin 2024, un client e-commerce suisse a vu son trafic Google s’effondrer de 73%. Diagnostic : Core Web Vitals en rouge depuis la mise à jour de l’algorithme de mai.

Métriques avant intervention

Après 2 semaines d’optimisations

Voici exactement ce que j’ai fait.

LCP : Largest Contentful Paint (< 2.5s)

Problème #1 : Images non optimisées

Avant

<img src="/hero.jpg" alt="Hero">
<!-- 2.8MB JPEG, 4000x3000px -->

Après

<picture>
  <source 
    srcset="/hero-800.webp 800w, /hero-1200.webp 1200w, /hero-1600.webp 1600w"
    type="image/webp"
  >
  <img 
    src="/hero-1200.jpg" 
    alt="Hero"
    width="1200" 
    height="600"
    loading="eager"
    fetchpriority="high"
  >
</picture>
<!-- 85KB WebP, dimensions correctes -->

Gain LCP : -1.8s

Script de conversion automatique

# Installer sharp
npm install sharp-cli -g

# Convertir toutes les images hero
for img in public/images/hero-*.jpg; do
  sharp -i "$img" -o "${img%.jpg}.webp" --webp quality=85
  sharp -i "$img" --resize 800 -o "${img%.jpg}-800.webp"
  sharp -i "$img" --resize 1200 -o "${img%.jpg}-1200.webp"
  sharp -i "$img" --resize 1600 -o "${img%.jpg}-1600.webp"
done

Problème #2 : Fonts bloquantes

Avant

<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<!-- Bloque le render pendant ~400ms -->

Après

<!-- 1. Preconnect -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

<!-- 2. Font display swap -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">

<!-- 3. Mieux : self-host -->
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
<style>
  @font-face {
    font-family: 'Inter';
    src: url('/fonts/inter-var.woff2') format('woff2');
    font-display: swap;
  }
</style>

Gain LCP : -600ms

Problème #3 : CSS bloquant

Avant : 1 fichier CSS de 280KB chargé en <head>

Après : Critical CSS inline + CSS asynchrone

<head>
  <!-- Critical CSS inline (< 14KB) -->
  <style>
    /* Styles above-the-fold uniquement */
    body { margin: 0; font-family: Inter, sans-serif; }
    .hero { height: 600px; background: #1a1a1a; }
    /* ... */
  </style>
  
  <!-- CSS complet en async -->
  <link rel="preload" href="/style.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
  <noscript><link rel="stylesheet" href="/style.css"></noscript>
</head>

Outil pour extraire le critical CSS

npm install critical -g

critical index.html --base ./ --inline --minify \
  --width 1920 --height 1080 \
  > critical.css

Gain LCP : -800ms

FID : First Input Delay (< 100ms)

Problème : JavaScript bloque le main thread

Audit

// Dans Chrome DevTools > Performance
// Identifier les Long Tasks (> 50ms)

Solution : Code splitting + lazy loading

Avant

import Analytics from './analytics';
import ChatWidget from './chat-widget';
import VideoPlayer from './video-player';
// Tout chargé au démarrage → 340KB JS

Après

// Charger uniquement le nécessaire
const loadAnalytics = () => import('./analytics');
const loadChat = () => import('./chat-widget');
const loadVideo = () => import('./video-player');

// Analytics : après 3s
setTimeout(loadAnalytics, 3000);

// Chat : au scroll
const observer = new IntersectionObserver((entries) => {
  if (entries[0].isIntersecting) loadChat();
});
observer.observe(document.querySelector('.chat-trigger'));

// Video : au clic
document.querySelector('.play-btn').addEventListener('click', async () => {
  const { default: VideoPlayer } = await loadVideo();
  new VideoPlayer().play();
});

Gain FID : -95ms

CLS : Cumulative Layout Shift (< 0.1)

Problème #1 : Images sans dimensions

Avant

<img src="/product.jpg" alt="Product">
<!-- Le navigateur ne connaît pas la taille → layout shift quand l'image charge -->

Après

<img 
  src="/product.jpg" 
  alt="Product"
  width="600" 
  height="400"
  style="aspect-ratio: 600/400; max-width: 100%; height: auto;"
>
<!-- Espace réservé → pas de layout shift -->

Problème #2 : Fonts qui changent la mise en page

Solution : font-size-adjust

body {
  font-family: 'Inter', Arial, sans-serif;
  font-size-adjust: 0.5; /* Évite le FOUT */
}

Problème #3 : Bannières/alertes dynamiques

Avant

// Bannière cookie ajoutée après chargement
setTimeout(() => {
  document.body.insertAdjacentHTML('afterbegin', '<div class="cookie-banner">...</div>');
}, 1000);
// → CLS de 0.15

Après

<!-- Réserver l'espace dès le HTML -->
<div class="cookie-banner-placeholder" style="height: 60px;"></div>
<script>
  // Remplacer le placeholder
  document.querySelector('.cookie-banner-placeholder').outerHTML = '...';
</script>

Gain CLS : -0.18

Checklist d’optimisation (30 minutes)

Mesurer (PageSpeed Insights)
[ ] Desktop + Mobile
[ ] Noter LCP, FID, CLS actuels

Images
[ ] Formats next-gen (WebP/AVIF)
[ ] Dimensions explicites (width/height)
[ ] Lazy loading (sauf hero)
[ ] Responsive images (srcset)

Fonts
[ ] Preconnect aux CDN
[ ] font-display: swap
[ ] Envisager self-hosting

CSS
[ ] Critical CSS inline
[ ] CSS non-critique async
[ ] Minification

JavaScript
[ ] Code splitting
[ ] Lazy load non-essentiel
[ ] Defer scripts tiers

Re-mesurer
[ ] Valider gains
[ ] Tester sur vraie 3G/4G

Outils indispensables

  1. PageSpeed Insights : https://pagespeed.web.dev/
  2. WebPageTest : Test sur vraie connexion mobile
  3. Chrome DevTools : Performance tab
  4. Lighthouse CI : Intégrer dans CI/CD
  5. Cloudflare : CDN + auto-optimisations

Résultats business

E-commerce suisse (6 semaines après optimisations)

ROI : ~8,500 CHF investis, +87,000 CHF de CA additionnel sur 3 mois.

Quand optimiser ?

Priorité haute si :

Fréquence : audit tous les 3-6 mois (nouvelles features dégradent souvent les métriques).


Besoin d’un audit de performance ? Contactez-moi pour un diagnostic gratuit de vos Core Web Vitals.