Aller au contenu principal
    IT INNOVE - Logo de l'agence de développement web et mobile
    Retour au blog
    Performance

    Core Web Vitals: gagner 20 points Lighthouse en 2 semaines

    Cléo Bauvois
    8 min de lecture

    Notre méthode pour améliorer LCP, INP et CLS sans refonte totale.

    Core Web Vitals: gagner 20 points Lighthouse en 2 semaines - Performance | IT INNOVE
    Core Web Vitals: gagner 20 points Lighthouse en 2 semaines - Performance

    Core Web Vitals: gagner 20 points Lighthouse en 2 semaines

    Optimiser les images, réduire le JS bloquant et réserver l'espace visuel suffisent souvent à franchir les seuils vitaux. Nous détaillons un plan d'audit et d'optimisation que nous avons industrialisé sur 30+ projets clients pour des gains rapides et mesurables.

    Les Core Web Vitals : impact business direct

    Pourquoi ces métriques comptent

    Google utilise les Core Web Vitals comme facteur de ranking depuis 2021. Au-delà du SEO, l'impact UX est mesurable :

    **Corrélations performance-business
    **
    • LCP < 2.5s : +15% de conversion moyenne
    • INP < 200ms : +25% d'engagement utilisateur
    • CLS < 0.1 : +30% de taux de complétion formulaire
    **Cas concret e-commerce
    **
    • Avant optimisation : LCP 4.8s, CLS 0.35
    • Après 2 semaines : LCP 1.9s, CLS 0.08
    • Résultat business : +28% de conversion mobile

    LCP (Largest Contentful Paint) : première impression critique

    Diagnostic rapide LCP

    javascript
    // Mesure LCP en temps réel
    import { getLCP } from 'web-vitals';
    
    getLCP((metric) => {
      console.log('LCP:', metric.value);
      // Identifier l'élément LCP
      console.log('LCP Element:', metric.entries[0].element);
      
      // Envoyer à analytics
      gtag('event', 'web_vitals', {
        event_category: 'Performance',
        event_label: 'LCP',
        value: Math.round(metric.value)
      });
    });

    Optimisation #1 : Images hero optimales

    **Problème courant
    ** 70% des LCP lents sont causés par des images mal optimisées.
    html
    <!-- ❌ Image hero non optimisée -->
    <img src="hero-banner.jpg" alt="Hero" />
    
    <!-- ✅ Image hero optimisée complète -->
    <picture>
      <!-- WebP moderne -->
      <source 
        srcset="hero-320w.webp 320w,
                hero-640w.webp 640w,
                hero-1024w.webp 1024w,
                hero-1920w.webp 1920w"
        sizes="(max-width: 768px) 100vw,
               (max-width: 1200px) 50vw,
               33vw"
        type="image/webp"
      />
      
      <!-- Fallback JPEG -->
      <source 
        srcset="hero-320w.jpg 320w,
                hero-640w.jpg 640w,
                hero-1024w.jpg 1024w,
                hero-1920w.jpg 1920w"
        sizes="(max-width: 768px) 100vw,
               (max-width: 1200px) 50vw,
               33vw"
      />
      
      <img 
        src="hero-1024w.jpg"
        alt="Description précise de l'image hero"
        width="1024"
        height="576"
        loading="eager"
        fetchpriority="high"
        decoding="sync"
      />
    </picture>
    **Script d'optimisation automatique
    **
    javascript
    // optimize-images.js
    const sharp = require('sharp');
    const fs = require('fs');
    
    const optimizeImage = async (inputPath, outputDir) => {
      const filename = path.basename(inputPath, path.extname(inputPath));
      const widths = [320, 640, 1024, 1920];
      
      // Générer WebP et JPEG responsive
      for (const width of widths) {
        // WebP
        await sharp(inputPath)
          .resize(width, null, { withoutEnlargement: true })
          .webp({ quality: 85 })
          .toFile(`${outputDir}/${filename}-${width}w.webp`);
          
        // JPEG
        await sharp(inputPath)
          .resize(width, null, { withoutEnlargement: true })
          .jpeg({ quality: 85, progressive: true })
          .toFile(`${outputDir}/${filename}-${width}w.jpg`);
      }
      
      console.log(`✅ Optimized: ${filename}`);
    };
    
    // Utilisation
    const inputDir = 'src/assets/images';
    const outputDir = 'public/images/optimized';
    
    fs.readdirSync(inputDir).forEach(file => {
      if (/\.(jpg|jpeg|png)$/i.test(file)) {
        optimizeImage(`${inputDir}/${file}`, outputDir);
      }
    });

    Optimisation #2 : Preload critique

    html
    <!-- Preload ressources critiques pour LCP -->
    <link rel="preload" as="image" href="/hero-1024w.webp" fetchpriority="high">
    <link rel="preload" as="font" href="/fonts/inter-var.woff2" type="font/woff2" crossorigin>
    <link rel="preload" as="style" href="/css/critical.css">
    
    <!-- DNS prefetch pour ressources externes -->
    <link rel="dns-prefetch" href="//fonts.googleapis.com">
    <link rel="dns-prefetch" href="//api.example.com">
    
    <!-- Preconnect pour domaines critiques -->
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

    Optimisation #3 : Critical CSS

    javascript
    // generate-critical-css.js
    const critical = require('critical');
    
    critical.generate({
      inline: true,
      base: 'dist/',
      src: 'index.html',
      dest: 'index-critical.html',
      width: 1300,
      height: 900,
      penthouse: {
        blockJSRequests: false,
      }
    }).then(() => {
      console.log('✅ Critical CSS generated');
    }).catch(err => {
      console.error('❌ Critical CSS error:', err);
    });
    **Implémentation manuelle critical CSS
    **
    html
    <head>
      <!-- Critical CSS inline -->
      <style>
        /* Above-the-fold styles only */
        .header { display: flex; justify-content: space-between; }
        .hero { min-height: 60vh; background: linear-gradient(...); }
        .hero h1 { font-size: 3rem; font-weight: 700; }
      </style>
      
      <!-- Non-critical CSS async -->
      <link rel="preload" href="/css/app.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
      <noscript><link rel="stylesheet" href="/css/app.css"></noscript>
    </head>

    INP (Interaction to Next Paint) : réactivité interface

    Diagnostic INP

    javascript
    // Mesure INP et identification des interactions lentes
    import { getINP } from 'web-vitals';
    
    getINP((metric) => {
      console.log('INP:', metric.value);
      
      // Log détaillé des interactions
      metric.entries.forEach((entry) => {
        console.log({
          type: entry.name,
          startTime: entry.startTime,
          processingStart: entry.processingStart,
          processingEnd: entry.processingEnd,
          duration: entry.duration
        });
      });
    });
    
    // Profiler les long tasks
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.duration > 50) {
          console.warn('Long task detected:', {
            duration: entry.duration,
            startTime: entry.startTime
          });
        }
      }
    });
    
    observer.observe({ entryTypes: ['longtask'] });

    Optimisation #1 : Code splitting intelligent

    javascript
    // Lazy loading des composants lourds
    import { lazy, Suspense } from 'react';
    
    const HeavyChart = lazy(() => import('./components/HeavyChart'));
    const DataTable = lazy(() => import('./components/DataTable'));
    
    const Dashboard = () => {
      const [showChart, setShowChart] = useState(false);
      
      return (
        <div>
          <h1>Dashboard</h1>
          
          <button onClick={() => setShowChart(true)}>
            Afficher graphique
          </button>
          
          {showChart && (
            <Suspense fallback={<ChartSkeleton />}>
              <HeavyChart />
            </Suspense>
          )}
        </div>
      );
    };
    
    // Preload au hover pour anticipation
    const PreloadButton = ({ onClick, children }) => {
      const handleMouseEnter = () => {
        // Preload le composant au hover
        import('./components/HeavyChart');
      };
      
      return (
        <button 
          onClick={onClick}
          onMouseEnter={handleMouseEnter}
        >
          {children}
        </button>
      );
    };

    Optimisation #2 : Web Workers pour calculs lourds

    javascript
    // worker.js - Traitement en background
    self.onmessage = function(e) {
      const { data, action } = e.data;
      
      switch (action) {
        case 'processLargeDataset':
          const result = processData(data); // Calcul lourd
          self.postMessage({ result });
          break;
          
        case 'generateReport':
          const report = generateReport(data);
          self.postMessage({ report });
          break;
      }
    };
    
    // Utilisation dans React
    const useWebWorker = () => {
      const [worker, setWorker] = useState(null);
      
      useEffect(() => {
        const workerInstance = new Worker('/worker.js');
        setWorker(workerInstance);
        
        return () => workerInstance.terminate();
      }, []);
      
      const processData = (data) => {
        return new Promise((resolve) => {
          worker.postMessage({ data, action: 'processLargeDataset' });
          worker.onmessage = (e) => resolve(e.data.result);
        });
      };
      
      return { processData };
    };

    Optimisation #3 : Debouncing et throttling

    javascript
    // Custom hooks optimisés
    import { useCallback, useRef } from 'react';
    
    const useDebounce = (callback, delay) => {
      const timeoutRef = useRef();
      
      return useCallback((...args) => {
        clearTimeout(timeoutRef.current);
        timeoutRef.current = setTimeout(() => callback(...args), delay);
      }, [callback, delay]);
    };
    
    const useThrottle = (callback, delay) => {
      const lastRun = useRef(Date.now());
      
      return useCallback((...args) => {
        if (Date.now() - lastRun.current >= delay) {
          callback(...args);
          lastRun.current = Date.now();
        }
      }, [callback, delay]);
    };
    
    // Usage pour recherche en temps réel
    const SearchInput = () => {
      const [query, setQuery] = useState('');
      const [results, setResults] = useState([]);
      
      const debouncedSearch = useDebounce(async (searchTerm) => {
        if (searchTerm.length > 2) {
          const data = await searchAPI(searchTerm);
          setResults(data);
        }
      }, 300);
      
      const handleInputChange = (e) => {
        const value = e.target.value;
        setQuery(value);
        debouncedSearch(value);
      };
      
      return (
        <input
          type="text"
          value={query}
          onChange={handleInputChange}
          placeholder="Rechercher..."
        />
      );
    };

    CLS (Cumulative Layout Shift) : stabilité visuelle

    Diagnostic CLS

    javascript
    // Mesure CLS et identification des shifts
    import { getCLS } from 'web-vitals';
    
    getCLS((metric) => {
      console.log('CLS:', metric.value);
      
      // Détail des layout shifts
      metric.entries.forEach((entry) => {
        console.log({
          value: entry.value,
          sources: entry.sources?.map(source => ({
            node: source.node,
            previousRect: source.previousRect,
            currentRect: source.currentRect
          }))
        });
      });
    });
    
    // Observer layout shifts en temps réel
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          console.warn('Layout shift detected:', entry);
        }
      }
    });
    
    observer.observe({ type: 'layout-shift', buffered: true });

    Optimisation #1 : Réservation d'espace

    css
    /* Skeleton loaders pour réserver l'espace */
    .image-placeholder {
      width: 400px;
      height: 300px;
      background: linear-gradient(
        90deg,
        #f0f0f0 25%,
        #e0e0e0 50%,
        #f0f0f0 75%
      );
      background-size: 200% 100%;
      animation: loading 1.5s infinite;
    }
    
    @keyframes loading {
      0% { background-position: 200% 0; }
      100% { background-position: -200% 0; }
    }
    
    /* Aspect ratio pour responsive images */
    .image-container {
      aspect-ratio: 16 / 9;
      overflow: hidden;
    }
    
    .image-container img {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
    jsx
    // Composant Image avec aspect ratio
    const OptimizedImage = ({ src, alt, width, height, className }) => {
      const [loaded, setLoaded] = useState(false);
      const aspectRatio = (height / width) * 100;
      
      return (
        <div 
          className={`relative ${className}`}
          style={{ paddingBottom: `${aspectRatio}%` }}
        >
          {!loaded && (
            <div className="absolute inset-0 image-placeholder" />
          )}
          
          <img
            src={src}
            alt={alt}
            className="absolute inset-0 w-full h-full object-cover"
            onLoad={() => setLoaded(true)}
            width={width}
            height={height}
          />
        </div>
      );
    };

    Optimisation #2 : Polices sans FOIT/FOUT

    css
    /* Préchargement des polices critiques */
    @font-face {
      font-family: 'Inter Variable';
      src: url('/fonts/inter-var.woff2') format('woff2');
      font-weight: 100 900;
      font-display: swap; /* Évite le FOIT */
      font-style: normal;
    }
    
    /* Fallback avec métriques similaires */
    body {
      font-family: 'Inter Variable', 
                   'SF Pro Display', 
                   system-ui, 
                   -apple-system, 
                   sans-serif;
    }
    
    /* Size adjust pour correspondance exacte */
    @font-face {
      font-family: 'Inter Fallback';
      src: local('Arial');
      size-adjust: 106.5%;
      ascent-override: 90%;
      descent-override: 22%;
      line-gap-override: 0%;
    }

    Optimisation #3 : Gestion des publicités

    javascript
    // Lazy loading des ads avec réservation d'espace
    const AdSlot = ({ width, height, adUnitId }) => {
      const [loaded, setLoaded] = useState(false);
      const adRef = useRef();
      
      useEffect(() => {
        const observer = new IntersectionObserver(
          (entries) => {
            if (entries[0].isIntersecting && !loaded) {
              loadAd(adUnitId, adRef.current);
              setLoaded(true);
            }
          },
          { rootMargin: '100px' }
        );
        
        if (adRef.current) {
          observer.observe(adRef.current);
        }
        
        return () => observer.disconnect();
      }, [adUnitId, loaded]);
      
      return (
        <div
          ref={adRef}
          className="ad-slot"
          style={{
            width: `${width}px`,
            height: `${height}px`,
            backgroundColor: '#f5f5f5',
            border: '1px solid #ddd',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center'
          }}
        >
          {!loaded && <span>Publicité</span>}
        </div>
      );
    };

    Plan d'optimisation 2 semaines

    Semaine 1 : Quick wins et diagnostics

    **Jour 1-2
    Audit complet**
    bash
    # Audit Lighthouse automatisé
    lighthouse https://yoursite.com --output json --output-path audit-before.json
    
    # Test sur plusieurs pages
    pages=("/" "/about" "/products" "/contact")
    for page in "${pages[@]}"; do
      lighthouse "https://yoursite.com$page" --output json --output-path "audit-$page.json"
    done
    **Jour 3-4
    Optimisations images**
    • Compression et conversion WebP
    • Responsive images avec srcset
    • Preload images critiques
    **Jour 5-7
    CSS et fonts**
    • Critical CSS extraction
    • Font optimization (preload, display: swap)
    • Suppression CSS inutilisé

    Semaine 2 : JavaScript et monitoring

    **Jour 8-10
    JavaScript**
    • Code splitting
    • Lazy loading composants
    • Web Workers pour calculs lourds
    **Jour 11-12
    Layout stability**
    • Aspect ratios fixes
    • Skeleton loaders
    • Dimensions explicites
    **Jour 13-14
    Tests et monitoring**
    • Tests sur devices réels
    • Monitoring continu setup
    • Validation gains

    Monitoring continu

    Setup monitoring production

    javascript
    // web-vitals-monitor.js
    import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
    
    const vitalsUrl = '/api/vitals';
    
    function sendToAnalytics(metric, additionalData = {}) {
      const body = JSON.stringify({
        name: metric.name,
        value: metric.value,
        id: metric.id,
        delta: metric.delta,
        url: window.location.href,
        userAgent: navigator.userAgent,
        timestamp: Date.now(),
        ...additionalData
      });
    
      // Envoi via Beacon API (non-bloquant)
      if ('sendBeacon' in navigator) {
        navigator.sendBeacon(vitalsUrl, body);
      } else {
        fetch(vitalsUrl, { 
          method: 'POST', 
          body, 
          keepalive: true,
          headers: { 'Content-Type': 'application/json' }
        });
      }
    }
    
    // Mesure toutes les métriques
    getCLS(sendToAnalytics);
    getFID(sendToAnalytics);
    getFCP(sendToAnalytics);
    getLCP(sendToAnalytics);
    getTTFB(sendToAnalytics);
    
    // Contexte additionnel
    const getDeviceInfo = () => ({
      connection: navigator.connection?.effectiveType,
      memory: navigator.deviceMemory,
      cores: navigator.hardwareConcurrency
    });
    
    getLCP((metric) => {
      sendToAnalytics(metric, {
        device: getDeviceInfo(),
        elementType: metric.entries[0]?.element?.tagName
      });
    });

    Dashboard monitoring

    sql
    -- Requêtes analytics pour dashboard
    -- Performance par page
    SELECT 
      url,
      AVG(CASE WHEN name = 'LCP' THEN value END) as avg_lcp,
      AVG(CASE WHEN name = 'INP' THEN value END) as avg_inp,
      AVG(CASE WHEN name = 'CLS' THEN value END) as avg_cls,
      COUNT(*) as samples
    FROM web_vitals 
    WHERE timestamp > DATE_SUB(NOW(), INTERVAL 7 DAY)
    GROUP BY url
    ORDER BY samples DESC;
    
    -- Évolution dans le temps
    SELECT 
      DATE(FROM_UNIXTIME(timestamp/1000)) as date,
      name,
      PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY value) as p75
    FROM web_vitals
    WHERE timestamp > DATE_SUB(NOW(), INTERVAL 30 DAY)
    GROUP BY date, name
    ORDER BY date DESC;

    ROI et résultats attendus

    Gains typiques observés

    **Performance metrics
    **
    • LCP : 4.2s → 2.1s (-50%)
    • INP : 340ms → 180ms (-47%)
    • CLS : 0.25 → 0.06 (-76%)
    • Lighthouse : 65 → 87 points
    **Business impact
    **
    • Conversion mobile : +25%
    • Bounce rate : -35%
    • Pages/session : +40%
    • SEO ranking : +15 positions moyenne

    Coût vs bénéfice

    **Investissement optimisation
    **
    • Audit et plan : 2j
    • Implémentation : 8-10j
    • Tests et monitoring : 2j
    • Total : 12-14j développeur
    **Retour estimé (e-commerce 1M€ CA)
    **
    • Conversion +25% = +250k€/an
    • SEO +15 positions = +150k€/an
    • ROI : 15-20x sur 12 mois

    Votre site souffre de Core Web Vitals médiocres qui limitent vos conversions ? Notre méthode éprouvée peut améliorer vos métriques de 40-60% en 2 semaines. Contactez-nous pour un audit performance gratuit et boostez votre business dès maintenant.

    Questions fréquentes