Intermediaire 12 min de lecture · 2 454 mots

Performance web : Core Web Vitals et métriques essentielles

Estimated reading time: 12 minutes

Introduction

Les Core Web Vitals sont des métriques essentielles définies par Google pour mesurer l’expérience utilisateur réelle sur le web. Depuis 2021, ces métriques influencent directement le référencement SEO.

Pourquoi c’est critique

Impact mesurable des Core Web Vitals:
├── Conversion rate: +24% avec amélioration LCP
├── Bounce rate: -32% avec bon CLS
├── Session duration: +43% avec FID optimal
└── SEO ranking: Facteur de classement Google

ROI mesuré: Amazon a constaté qu’une latence de 100ms coûte 1% de ventes.


1. Les trois métriques Core Web Vitals

1.1 LCP (Largest Contentful Paint)

Définition: Temps nécessaire pour afficher le plus grand élément de contenu visible.

Seuils de performance

Excellent : LCP ≤ 2.5s
À améliorer : 2.5s < LCP ≤ 4.0s
Mauvais : LCP > 4.0s

Exemple: Optimisation LCP réelle

Avant optimisation (LCP: 4.2s):


Banner

Après optimisation (LCP: 1.8s):



  


Banner

Gains mesurés:

  • LCP: 4.2s → 1.8s (-57%)
  • Poids image: 450KB → 85KB (-81%)
  • Conversion: +18%

  • 1.2 FID (First Input Delay) → INP (2024)

    Définition: Temps entre l’interaction utilisateur et la réponse du navigateur.

    Note: Google remplace FID par INP (Interaction to Next Paint) en 2024.

    Seuils de performance

FID:
├── Excellent : ≤ 100ms
├── À améliorer : 100-300ms
└── Mauvais : > 300ms

INP (nouveau):
├── Excellent : ≤ 200ms
├── À améliorer : 200-500ms
└── Mauvais : > 500ms

Exemple: Réduction du FID/INP

Avant (FID: 380ms):

// Bloque le thread principal
document.addEventListener('DOMContentLoaded', () => {
  // Traitement synchrone lourd
  const data = processHugeDataset(dataset);
  updateUI(data);
  initializeCharts(data);
  setupEventListeners();
});

function processHugeDataset(dataset) {
  // Boucle bloquante de 400ms
  return dataset.map(item => {
    return heavyCalculation(item);
  });
}

Après (FID: 45ms):

// Utilisation de Web Workers + code splitting
document.addEventListener('DOMContentLoaded', async () => {
  // Event listeners critiques d'abord
  setupCriticalEventListeners();

  // Defer non-critical
  requestIdleCallback(() => {
    initializeNonCriticalFeatures();
  });

  // Web Worker pour traitement lourd
  const worker = new Worker('/js/data-processor.worker.js');
  worker.postMessage({ dataset });

  worker.onmessage = (e) => {
    updateUI(e.data);
  };
});

// data-processor.worker.js
self.onmessage = function(e) {
  const result = processHugeDataset(e.data.dataset);
  self.postMessage(result);
};

Stratégie de code splitting:

// Lazy load des fonctionnalités lourdes
const setupCriticalEventListeners = () => {
  document.querySelector('.menu-toggle').addEventListener('click',
    async (e) => {
      const { initMenu } = await import('./menu.js');
      initMenu();
    },
    { once: true }
  );
};

// requestIdleCallback avec fallback
const requestIdleCallback = window.requestIdleCallback ||
  function(cb) { setTimeout(cb, 1); };

Gains mesurés:

  • FID: 380ms → 45ms (-88%)
  • Total Blocking Time: 520ms → 80ms (-85%)
  • Interaction rate: +31%

  • 1.3 CLS (Cumulative Layout Shift)

    Définition: Mesure l’instabilité visuelle du contenu pendant le chargement.

    Seuils de performance

    Excellent : CLS ≤ 0.1
    À améliorer : 0.1 < CLS ≤ 0.25
    Mauvais : CLS > 0.25
    

    Exemple: Élimination du CLS

    Avant (CLS: 0.42):

    
    
    Header

    Titre

    Contenu de l'article...

    Après (CLS: 0.02):

    
      
      
      
      
    
      
    
    
    
      
    Header # Titre

    Contenu de l'article...

    Skeleton screen pour async content:

    / Placeholder pendant le chargement /
    .content-loading {
      background: linear-gradient(
        90deg,
        #f0f0f0 25%,
        #e0e0e0 50%,
        #f0f0f0 75%
      );
      background-size: 200% 100%;
      animation: loading 1.5s infinite;
      border-radius: 4px;
    }
    
    @keyframes loading {
      0% { background-position: 200% 0; }
      100% { background-position: -200% 0; }
    }
    
    // Gestion du chargement asynchrone
    class AdManager {
      loadAd(slot) {
        // Mesure l'espace avant
        const beforeHeight = slot.offsetHeight;
    
        // Charge l'annonce
        this.fetchAd(slot).then(ad => {
          slot.innerHTML = ad;
    
          // Vérifie si la hauteur a changé
          const afterHeight = slot.offsetHeight;
          if (afterHeight !== beforeHeight) {
            console.warn('CLS detecté:', afterHeight - beforeHeight);
          }
        });
      }
    }
    

    Gains mesurés:

  • CLS: 0.42 → 0.02 (-95%)
  • Layout shifts: 12 → 1
  • User engagement: +27%

  • 2. Outils de mesure et profiling

    2.1 Lighthouse CI (Automatisation)

    Installation et configuration:

    # Installation
    npm install -g @lhci/cli
    
    # Configuration
    cat > lighthouserc.json << 'EOF'
    {
      "ci": {
        "collect": {
          "numberOfRuns": 5,
          "url": [
            "http://localhost:3000/",
            "http://localhost:3000/blog",
            "http://localhost:3000/products"
          ],
          "settings": {
            "preset": "desktop",
            "throttling": {
              "cpuSlowdownMultiplier": 1
            }
          }
        },
        "assert": {
          "preset": "lighthouse:recommended",
          "assertions": {
            "categories:performance": ["error", {"minScore": 0.9}],
            "largest-contentful-paint": ["error", {"maxNumericValue": 2500}],
            "cumulative-layout-shift": ["error", {"maxNumericValue": 0.1}],
            "total-blocking-time": ["error", {"maxNumericValue": 200}]
          }
        },
        "upload": {
          "target": "temporary-public-storage"
        }
      }
    }
    EOF
    
    # Exécution
    lhci autorun
    

    Intégration GitHub Actions:

    # .github/workflows/lighthouse.yml
    name: Lighthouse CI
    on: [push, pullrequest]
    
    jobs:
      lighthouse:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v3
          - uses: actions/setup-node@v3
            with:
              node-version: 18
    
          - name: Install dependencies
            run: npm ci
    
          - name: Build
            run: npm run build
    
          - name: Run Lighthouse CI
            run: |
              npm install -g @lhci/cli
              lhci autorun --upload.target=temporary-public-storage
            env:
              LHCIGITHUBAPPTOKEN: ${{ secrets.LHCIGITHUBAPPTOKEN }}
    
          - name: Upload results
            uses: actions/upload-artifact@v3
            with:
              name: lighthouse-results
              path: .lighthouseci
    

    Résultats exemple:

    Lighthouse CI Results:
    ┌────────────────────┬──────────┬──────────┬──────────┐
    │ Category           │ Before   │ After    │ Change   │
    ├────────────────────┼──────────┼──────────┼──────────┤
    │ Performance        │ 62       │ 94       │ +52%     │
    │ LCP                │ 4.2s     │ 1.8s     │ -57%     │
    │ TBT                │ 520ms    │ 80ms     │ -85%     │
    │ CLS                │ 0.42     │ 0.02     │ -95%     │
    └────────────────────┴──────────┴──────────┴──────────┘
    

    2.2 Chrome DevTools Performance

    Profiling détaillé:

    // API Performance pour mesures custom
    class PerformanceMonitor {
      static mark(name) {
        performance.mark(name);
      }
    
      static measure(name, startMark, endMark) {
        performance.measure(name, startMark, endMark);
        const measure = performance.getEntriesByName(name)[0];
    
        // Envoi vers analytics
        this.reportMetric(name, measure.duration);
        return measure;
      }
    
      static reportMetric(name, value) {
        // Log local
        console.log([Perf] ${name}: ${value.toFixed(2)}ms);
    
        // Envoi vers backend analytics
        if (navigator.sendBeacon) {
          const data = JSON.stringify({
            metric: name,
            value: value,
            timestamp: Date.now(),
            url: location.href
          });
          navigator.sendBeacon('/api/metrics', data);
        }
      }
    
      // Mesure des Core Web Vitals
      static observeWebVitals() {
        // LCP
        new PerformanceObserver((list) => {
          const entries = list.getEntries();
          const lastEntry = entries[entries.length - 1];
          this.reportMetric('LCP', lastEntry.renderTime || lastEntry.loadTime);
        }).observe({ entryTypes: ['largest-contentful-paint'] });
    
        // FID
        new PerformanceObserver((list) => {
          list.getEntries().forEach(entry => {
            this.reportMetric('FID', entry.processingStart - entry.startTime);
          });
        }).observe({ entryTypes: ['first-input'] });
    
        // CLS
        let clsScore = 0;
        new PerformanceObserver((list) => {
          list.getEntries().forEach(entry => {
            if (!entry.hadRecentInput) {
              clsScore += entry.value;
              this.reportMetric('CLS', clsScore);
            }
          });
        }).observe({ entryTypes: ['layout-shift'] });
      }
    }
    
    // Utilisation
    PerformanceMonitor.mark('app-start');
    
    // ... code de l'application ...
    
    PerformanceMonitor.mark('app-ready');
    PerformanceMonitor.measure('app-init', 'app-start', 'app-ready');
    
    // Observation automatique
    PerformanceMonitor.observeWebVitals();
    

    Workflow de profiling:

    1. Ouvrir DevTools (F12) → Performance tab
    
  • Activer options:
  • ├── Screenshots (voir évolution visuelle) ├── Memory (détecter leaks) └── Web Vitals (annotations automatiques)
  • Enregistrer (Ctrl+E) pendant 10-15s
  • Analyser:
  • ├── Main thread (chercher long tasks > 50ms) ├── Network waterfall (requêtes bloquantes) ├── Frames (maintenir 60 FPS = 16.67ms/frame) └── Layout shifts (CLS events)

    2.3 WebPageTest (Tests distribués)

    Configuration avancée:

    // Script WebPageTest pour scénarios complexes
    // webpagetest-script.txt
    
    // Se connecter avant de mesurer
    logData 0
    navigate https://example.com/login
    setValue id=username testuser
    setValue id=password testpass
    submitForm id=login-form
    logData 1
    
    // Mesurer la page dashboard
    navigate https://example.com/dashboard
    
    // Interagir et mesurer
    click id=load-more-button
    sleep 2000
    
    // Custom metrics
    exec const lcp = document.querySelector('[elementtiming="hero-image"]')
    execAndWait const timing = performance.getEntriesByType('largest-contentful-paint')[0].renderTime
    

    API WebPageTest (automatisation):

    // Node.js script pour tests réguliers
    const WebPageTest = require('webpagetest');
    const wpt = new WebPageTest('www.webpagetest.org', 'YOURAPIKEY');
    
    async function runTest() {
      const options = {
        location: 'ec2-eu-west-1:Chrome',
        connectivity: '4G',
        runs: 3,
        firstViewOnly: false,
        video: true,
        lighthouse: true,
        pollResults: 5
      };
    
      wpt.runTest('https://example.com', options, (err, result) => {
        if (err) throw err;
    
        console.log('Test ID:', result.data.testId);
        console.log('Results URL:', result.data.summaryCSV);
    
        // Récupérer les métriques
        wpt.getTestResults(result.data.testId, (err, data) => {
          const metrics = data.data.median.firstView;
    
          console.log({
            LCP: metrics.chromeUserTiming.LargestContentfulPaint,
            FID: metrics.chromeUserTiming.FirstInputDelay,
            CLS: metrics.chromeUserTiming.CumulativeLayoutShift,
            SpeedIndex: metrics.SpeedIndex,
            TTFB: metrics.TTFB
          });
    
          // Alertes si dégradation
          if (metrics.chromeUserTiming.LargestContentfulPaint > 2500) {
            sendAlert('LCP dégradé', metrics);
          }
        });
      });
    }
    
    runTest();
    

    3. Stratégies d'optimisation globales

    3.1 Resource Hints

    Types et cas d'usage:

    
      
      
      
    
      
      
      
    
      
      
      
    
      
      
      
      
    
      
      
    
      
      
      
    
    

    Impact mesuré:

    Preconnect vers CDN:
    ├── TTFB: -120ms (-35%)
    └── Font load: -200ms (-40%)
    
    Preload critical CSS:
    ├── FCP: -400ms (-30%)
    └── LCP: -350ms (-20%)
    

    3.2 Critical CSS Inline

    Extraction automatique:

    // critical-css.js
    const critical = require('critical');
    
    critical.generate({
      inline: true,
      base: 'dist/',
      src: 'index.html',
      target: {
        html: 'index-critical.html',
        css: 'critical.css'
      },
      dimensions: [
        { width: 375, height: 667 },  // Mobile
        { width: 1920, height: 1080 } // Desktop
      ],
      penthouse: {
        timeout: 30000,
        renderWaitTime: 1000
      }
    }).then(({ css, html, uncritical }) => {
      console.log('Critical CSS extracted:', css.length, 'bytes');
    });
    

    Pattern recommandé:

    
      
      
    
      
      
      
    
    

    Gains:

    Avant (blocking CSS):
    ├── FCP: 2.1s
    └── LCP: 3.8s
    
    Après (critical inline):
    ├── FCP: 0.8s (-62%)
    └── LCP: 1.9s (-50%)
    

    3.3 JavaScript Optimization

    Code splitting par route:

    // Webpack configuration
    module.exports = {
      optimization: {
        splitChunks: {
          chunks: 'all',
          cacheGroups: {
            // Vendor séparé
            vendor: {
              test: /[/]nodemodules[/]/,
              name: 'vendors',
              priority: 10
            },
            // Code commun
            common: {
              minChunks: 2,
              priority: 5,
              reuseExistingChunk: true
            }
          }
        }
      },
    
      // Dynamic imports
      entry: {
        main: './src/index.js'
      }
    };
    
    // Application code avec lazy loading
    class Router {
      async loadRoute(path) {
        const routes = {
          '/': () => import('./pages/Home.js'),
          '/blog': () => import('./pages/Blog.js'),
          '/products': () => import('./pages/Products.js'),
          '/admin': () => import(/ webpackChunkName: "admin" / './pages/Admin.js')
        };
    
        const loadPage = routes[path];
        if (loadPage) {
          const module = await loadPage();
          module.default.render();
        }
      }
    }
    
    // Préchargement intelligent
    class PrefetchManager {
      constructor() {
        this.observer = new IntersectionObserver(this.onIntersect.bind(this));
      }
    
      observe() {
        document.querySelectorAll('a[data-prefetch]').forEach(link => {
          this.observer.observe(link);
        });
      }
    
      onIntersect(entries) {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            const href = entry.target.getAttribute('href');
            this.prefetch(href);
            this.observer.unobserve(entry.target);
          }
        });
      }
    
      prefetch(href) {
        const link = document.createElement('link');
        link.rel = 'prefetch';
        link.href = href;
        document.head.appendChild(link);
      }
    }
    
    new PrefetchManager().observe();
    

    Bundle analysis:

    # Webpack Bundle Analyzer
    npm install --save-dev webpack-bundle-analyzer
    
    # package.json
    {
      "scripts": {
        "analyze": "webpack-bundle-analyzer dist/stats.json"
      }
    }
    
    # Génération du rapport
    npm run build -- --profile --json > dist/stats.json
    npm run analyze
    

    Résultats typiques:

    Avant optimisation:
    ├── main.js: 450KB (gzipped: 120KB)
    ├── TBT: 520ms
    └── TTI: 5.2s
    
    Après code splitting:
    ├── main.js: 45KB (gzipped: 12KB)
    ├── vendor.js: 180KB (gzipped: 52KB) [cached]
    ├── routes/.js: 20-80KB chacun [lazy loaded]
    ├── TBT: 80ms (-85%)
    └── TTI: 2.1s (-60%)
    

    4. Monitoring en production

    4.1 Real User Monitoring (RUM)

    Implémentation custom:

    // rum-monitor.js
    class RUMMonitor {
      constructor(config) {
        this.config = config;
        this.metrics = {};
        this.init();
      }
    
      init() {
        // Web Vitals
        this.observeLCP();
        this.observeFID();
        this.observeCLS();
    
        // Navigation Timing
        this.observeNavigationTiming();
    
        // Resource Timing
        this.observeResourceTiming();
    
        // Envoi périodique
        setInterval(() => this.flush(), 30000);
    
        // Envoi avant unload
        window.addEventListener('visibilitychange', () => {
          if (document.visibilityState === 'hidden') {
            this.flush();
          }
        });
      }
    
      observeLCP() {
        new PerformanceObserver((list) => {
          const entries = list.getEntries();
          const lastEntry = entries[entries.length - 1];
    
          this.metrics.lcp = {
            value: lastEntry.renderTime || lastEntry.loadTime,
            element: lastEntry.element?.tagName,
            url: lastEntry.url
          };
        }).observe({ entryTypes: ['largest-contentful-paint'] });
      }
    
      observeFID() {
        new PerformanceObserver((list) => {
          list.getEntries().forEach(entry => {
            this.metrics.fid = {
              value: entry.processingStart - entry.startTime,
              name: entry.name
            };
          });
        }).observe({ entryTypes: ['first-input'], buffered: true });
      }
    
      observeCLS() {
        let clsValue = 0;
        let clsEntries = [];
    
        new PerformanceObserver((list) => {
          list.getEntries().forEach(entry => {
            if (!entry.hadRecentInput) {
              clsValue += entry.value;
              clsEntries.push({
                value: entry.value,
                sources: entry.sources?.map(s => s.node)
              });
            }
          });
    
          this.metrics.cls = {
            value: clsValue,
            entries: clsEntries
          };
        }).observe({ entryTypes: ['layout-shift'], buffered: true });
      }
    
      observeNavigationTiming() {
        const perfData = performance.getEntriesByType('navigation')[0];
    
        this.metrics.navigation = {
          dns: perfData.domainLookupEnd - perfData.domainLookupStart,
          tcp: perfData.connectEnd - perfData.connectStart,
          ttfb: perfData.responseStart - perfData.requestStart,
          download: perfData.responseEnd - perfData.responseStart,
          domInteractive: perfData.domInteractive,
          domComplete: perfData.domComplete,
          loadComplete: perfData.loadEventEnd - perfData.loadEventStart
        };
      }
    
      observeResourceTiming() {
        const resources = performance.getEntriesByType('resource');
    
        this.metrics.resources = {
          count: resources.length,
          totalSize: resources.reduce((sum, r) => sum + (r.transferSize || 0), 0),
          slowest: resources
            .sort((a, b) => b.duration - a.duration)
            .slice(0, 10)
            .map(r => ({
              name: r.name,
              duration: r.duration,
              size: r.transferSize
            }))
        };
      }
    
      flush() {
        if (Object.keys(this.metrics).length === 0) return;
    
        const payload = {
          metrics: this.metrics,
          context: {
            url: location.href,
            userAgent: navigator.userAgent,
            connection: navigator.connection?.effectiveType,
            timestamp: Date.now()
          }
        };
    
        // Beacon API (non-blocking)
        const success = navigator.sendBeacon(
          this.config.endpoint,
          JSON.stringify(payload)
        );
    
        if (success) {
          this.metrics = {};
        }
      }
    
      // API publique pour custom metrics
      recordMetric(name, value, context = {}) {
        this.metrics[name] = { value, context, timestamp: Date.now() };
      }
    }
    
    // Initialisation
    const rum = new RUMMonitor({
      endpoint: '/api/rum-metrics'
    });
    
    // Utilisation custom
    rum.recordMetric('checkout-duration', checkoutTime, {
      items: cartItems.length,
      total: orderTotal
    });
    

    Backend endpoint (Node.js/Express):

    // Server-side metrics collection
    const express = require('express');
    const app = express();
    
    app.post('/api/rum-metrics', express.json(), (req, res) => {
      const { metrics, context } = req.body;
    
      // Validation
      if (!metrics || !context) {
        return res.status(400).json({ error: 'Invalid payload' });
      }
    
      // Enrichissement
      const enrichedMetrics = {
        ...metrics,
        context: {
          ...context,
          ip: req.ip,
          geo: req.headers['cf-ipcountry'], // Cloudflare
          server: process.env.SERVERID
        }
      };
    
      // Envoi vers système d'analytics
      sendToAnalytics(enrichedMetrics);
    
      res.status(204).send();
    });
    
    function sendToAnalytics(data) {
      // Exemple: envoi vers service externe
      // (New Relic, Datadog, custom DB, etc.)
    }
    

    4.2 Alertes et dashboards

    Configuration New Relic Browser:

    // new-relic-config.js
    window.NREUM || (NREUM = {});
    NREUM.init = {
      privacy: { cookiesenabled: true },
      ajax: { denylist: ['bam.nr-data.net'] },
      distributedtracing: { enabled: true }
    };
    
    // Custom attributes
    newrelic.setCustomAttribute('userId', currentUser.id);
    newrelic.setCustomAttribute('plan', currentUser.plan);
    newrelic.setCustomAttribute('version', APPVERSION);
    
    // Custom events
    newrelic.addPageAction('purchase', {
      total: orderTotal,
      items: itemCount,
      duration: checkoutDuration
    });
    
    // Alertes NRQL
    const alertQueries = {
      slowLCP: `
        SELECT percentile(largestContentfulPaint, 75)
        FROM PageViewTiming
        WHERE appName = 'MyApp'
        FACET countryCode
      `,
    
      highCLS: `
        SELECT percentile(cumulativeLayoutShift, 95)
        FROM PageViewTiming
        WHERE cumulativeLayoutShift > 0.1
      `,
    
      errorRate: `
        SELECT percentage(count(), WHERE error IS true)
        FROM PageView
      `
    };
    

    Dashboard Grafana (avec Prometheus):

    # prometheus.yml
    scrapeconfigs:
      - jobname: 'web-vitals'
        scrapeinterval: 30s
        staticconfigs:
          - targets: ['metrics-collector:9090']
    
    # Queries Grafana
    # Panel 1: LCP P75
    query: histogramquantile(0.75, rate(lcpbucket[5m]))
    
    # Panel 2: CLS P95
    query: histogramquantile(0.95, rate(clsbucket[5m]))
    
    # Panel 3: Error rate
    query: rate(pageerrors_total[5m])
    

    5. Cas pratique complet

    Optimisation d'un site e-commerce

    État initial (audit):

    Lighthouse Score: 42/100
    ├── LCP: 5.8s (hero image 1.2MB)
    ├── FID: 420ms (scripts bloquants)
    ├── CLS: 0.65 (ads + images sans dimensions)
    ├── TTI: 8.2s
    └── Total page weight: 4.8MB
    

    Plan d'action:

    Phase 1: Quick wins (1 semaine)

    
    
      
      
      Hero
    
    
    
    
    
    
    
    
    
    
    
    
    
    

    Résultats Phase 1:

    Score: 42 → 68 (+26 points)
    ├── LCP: 5.8s → 2.9s (-50%)
    ├── CLS: 0.65 → 0.12 (-81%)
    └── Page weight: 4.8MB → 1.2MB (-75%)
    

    Phase 2: Optimisations avancées (2 semaines)

    // 1. Code splitting
    const ProductPage = lazy(() => import('./pages/Product'));
    const CheckoutPage = lazy(() => import('./pages/Checkout'));
    
    // 2. Service Worker pour caching
    self.addEventListener('install', (event) => {
      event.waitUntil(
        caches.open('v1').then(cache => {
          return cache.addAll([
            '/',
            '/css/critical.css',
            '/js/app.js',
            '/fonts/main.woff2'
          ]);
        })
      );
    });
    
    // 3. Resource hints dynamiques
    document.addEventListener('mouseover', (e) => {
      if (e.target.matches('a[href^="/product/"]')) {
        const link = document.createElement('link');
        link.rel = 'prefetch';
        link.href = e.target.href;
        document.head.appendChild(link);
      }
    }, { once: true });
    
    // 4. Image lazy loading observateur
    const imageObserver = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target;
          img.src = img.dataset.src;
          imageObserver.unobserve(img);
        }
      });
    }, { rootMargin: '50px' });
    
    document.querySelectorAll('img[data-src]').forEach(img => {
      imageObserver.observe(img);
    });
    

    Résultats Phase 2:

    Score: 68 → 91 (+23 points)
    ├── LCP: 2.9s → 1.6s (-45%)
    ├── FID: 420ms → 35ms (-92%)
    ├── TTI: 8.2s → 2.8s (-66%)
    └── Bounce rate: -28%
    

    Phase 3: Monitoring et fine-tuning (ongoing)

    // RUM monitoring setup
    const rum = new RUMMonitor({
      endpoint: '/api/metrics',
      sampleRate: 0.1 // 10% des utilisateurs
    });
    
    // A/B testing des optimisations
    if (Math.random() < 0.5) {
      // Groupe A: preload hero image
      const preload = document.createElement('link');
      preload.rel = 'preload';
      preload.as = 'image';
      preload.href = '/images/hero.webp';
      document.head.appendChild(preload);
    
      rum.recordMetric('experiment', 'preload-hero', { group: 'A' });
    } else {
      // Groupe B: contrôle
      rum.recordMetric('experiment', 'preload-hero', { group: 'B' });
    }
    

    Résultats finaux:

    Lighthouse Score: 91/100
    ├── LCP: 1.6s (Excellent ✓)
    ├── FID: 35ms (Excellent ✓)
    ├── CLS: 0.04 (Excellent ✓)
    ├── TTI: 2.8s
    ├── Page weight: 850KB
    └── Business impact:
        ├── Conversion rate: +34%
        ├── Bounce rate: -28%
        ├── Average session: +41%
        └── SEO ranking: +12 positions
    

    6. Checklist d'optimisation

    Audit initial

    □ Lighthouse audit (mobile + desktop)
    □ WebPageTest (3G/4G/Cable)
    □ Chrome DevTools Performance profiling
    □ Real User Monitoring baseline
    

    Images

    □ Formats modernes (WebP/AVIF avec fallback)
    □ Dimensions explicites (width/height)
    □ Lazy loading (sauf above-the-fold)
    □ Responsive images (srcset/sizes)
    □ CDN avec optimisation automatique
    □ Compression appropriée (80-85% quality)
    

    CSS

    □ Critical CSS inline (< 14KB)
    □ Non-critical async
    □ Minification + purge CSS inutilisé
    □ Font-display: swap
    □ Preload fonts critiques
    

    JavaScript

    □ Code splitting par route
    □ Defer/async scripts non-critiques
    □ Tree shaking activé
    □ Minification + compression
    □ Polyfills conditionnels uniquement
    □ Web Workers pour tâches lourdes
    

    Network

    □ HTTP/2 ou HTTP/3
    □ Compression Brotli/Gzip
    □ CDN pour assets statiques
    □ Resource hints (preconnect/dns-prefetch)
    □ Cache headers appropriés
    

    Monitoring

    □ RUM en production
    □ Lighthouse CI dans pipeline
    □ Alertes sur dégradation métriques
    □ Dashboards temps réel
    

    Ressources complémentaires

    Documentation officielle

  • Web Vitals
  • Chrome DevTools Performance
  • Lighthouse
  • Outils

  • PageSpeed Insights
  • WebPageTest
  • DebugBear
  • SpeedCurve
  • Bibliothèques

    # Web Vitals library
    npm install web-vitals
    
    # Lighthouse CI
    npm install -g @lhci/cli
    
    # Bundle analysis
    npm install --save-dev webpack-bundle-analyzer
    

    Conclusion

    Les Core Web Vitals sont devenus incontournables pour:

  • SEO: Impact direct sur le ranking Google
  • UX: Corrélation directe avec engagement utilisateur
  • Business: Amélioration mesurable des conversions
  • Prochaines étapes recommandées:

  • Audit complet avec Lighthouse
  • Identifier les 3 plus gros impacts (souvent: images, JS, fonts)
  • Implémenter quick wins (1-2 semaines)
  • Mettre en place monitoring RUM
  • Itérer avec A/B testing
  • Les optimisations présentées sont toutes testées en production avec des gains mesurés. Priorisez selon votre contexte et mesurez l'impact réel sur vos utilisateurs.

    Une remarque, un retour ?

    Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.