Lighthouse CI : monitoring performance continu en production

Un déploiement anodin un mardi après-midi — une dépendance npm mise à jour, un composant hero refactoré — et le LCP de vos pages catégories passe de 1.8s à 4.2s. Personne ne s'en aperçoit pendant trois semaines, jusqu'à ce que le trafic organique décroche de 18%. Lighthouse CI existe précisément pour empêcher ce scénario.

Ce que Lighthouse CI fait (et ne fait pas)

Lighthouse CI (LHCI) est un wrapper autour de Lighthouse conçu pour s'intégrer dans les pipelines CI/CD. Il exécute des audits Lighthouse de façon automatisée à chaque commit, pull request ou déploiement, puis compare les résultats à des seuils prédéfinis ou à des runs historiques.

La différence avec un audit Lighthouse ponctuel

Un audit manuel dans Chrome DevTools ou via PageSpeed Insights vous donne un instantané. C'est utile pour diagnostiquer, mais ça ne prévient rien. Vous lancez Lighthouse quand vous y pensez — c'est-à-dire après que le problème est visible.

LHCI inverse la logique : chaque changement de code est audité automatiquement. Si le score performance descend sous un seuil, la CI échoue. Le développeur est alerté avant le merge, pas trois sprints plus tard quand un SEO constate la chute de trafic dans Search Console.

Ce que LHCI ne fait pas : il n'audite pas vos pages en conditions réelles d'utilisateur (c'est le rôle du CrUX / RUM). Il simule un environnement contrôlé (émulation mobile Moto G Power, throttling réseau). Les scores sont donc synthétiques — utiles pour détecter des régressions relatives entre deux déploiements, pas pour mesurer l'expérience terrain exacte.

Architecture de LHCI

Trois composants principaux :

  • LHCI CLI : l'outil en ligne de commande qui collecte les audits, les compare aux assertions, et les upload.
  • LHCI Server : une application Node.js avec une base SQLite/PostgreSQL qui stocke l'historique des runs et fournit un dashboard de comparaison.
  • Assertions : un système de règles qui transforme les résultats Lighthouse en pass/fail dans votre pipeline.

Vous pouvez utiliser LHCI sans le serveur (en comparant uniquement aux assertions statiques), mais le serveur est ce qui rend le monitoring réellement continu — il permet de visualiser les tendances sur des semaines et de comparer les résultats entre branches.

Installation et configuration pas à pas

Prérequis

LHCI tourne sur Node.js 16+. Il a besoin de Chrome/Chromium — la plupart des images CI Docker incluent déjà une version headless.

# Installation globale (CI runner)
npm install -g @lhci/[email protected]

# Ou en devDependency dans votre projet
npm install --save-dev @lhci/[email protected]

# Vérifier l'installation
lhci --version

# Si Chrome headless n'est pas disponible sur votre runner
# (ex: image Alpine minimale), installez Puppeteer avec Chromium bundlé
npm install puppeteer

Le fichier lighthouserc.js

Toute la configuration LHCI passe par un fichier lighthouserc.js (ou .lighthouserc.json, .lighthouserc.yml) à la racine du projet. Voici une configuration réaliste pour un e-commerce Next.js avec 12 000 pages :

// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      // URLs critiques à auditer — pas toutes les 12K pages,
      // mais un échantillon représentatif par template
      url: [
        'http://localhost:3000/',                          // Homepage
        'http://localhost:3000/c/chaussures-running',      // Page catégorie (listing)
        'http://localhost:3000/p/nike-pegasus-41-noir',     // Page produit (PDP)
        'http://localhost:3000/blog/guide-pronation',       // Article éditorial
        'http://localhost:3000/recherche?q=trail',          // Page recherche interne
      ],
      // Nombre de runs par URL — 3 minimum pour lisser la variance
      numberOfRuns: 5,
      // Démarrer le serveur local avant les audits
      startServerCommand: 'npm run start',
      startServerReadyPattern: 'ready on',
      startServerReadyTimeout: 30000,
      settings: {
        // Émulation identique à PageSpeed Insights mobile
        preset: 'desktop', // ou 'perf' pour mobile
        // Throttling personnalisé si nécessaire
        throttling: {
          cpuSlowdownMultiplier: 4,
          downloadThroughputKbps: 1600,
          uploadThroughputKbps: 750,
          rttMs: 150,
        },
        // Skip les catégories non pertinentes pour gagner du temps
        onlyCategories: ['performance', 'accessibility', 'seo'],
        // Chrome flags pour environnement CI headless
        chromeFlags: ['--no-sandbox', '--headless', '--disable-gpu'],
      },
    },
    assert: {
      assertions: {
        // Score global performance : warning sous 85, erreur sous 70
        'categories:performance': ['error', { minScore: 0.7 }],
        'categories:seo': ['error', { minScore: 0.9 }],
        'categories:accessibility': ['warn', { minScore: 0.8 }],

        // Métriques Core Web Vitals individuelles
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
        'total-blocking-time': ['error', { maxNumericValue: 300 }],

        // Détection de régressions spécifiques
        'first-contentful-paint': ['warn', { maxNumericValue: 1800 }],
        'speed-index': ['warn', { maxNumericValue: 3400 }],

        // Assertions SEO critiques
        'meta-description': 'error',
        'document-title': 'error',
        'http-status-code': 'error',
        'is-crawlable': 'error',
        'canonical': 'error',
        'hreflang': 'warn',
      },
    },
    upload: {
      // Option 1 : LHCI Server auto-hébergé
      target: 'lhci',
      serverBaseUrl: 'https://lhci.votredomaine.com',
      token: process.env.LHCI_BUILD_TOKEN,

      // Option 2 : temporary-public-storage (gratuit, données publiques, 7j de rétention)
      // target: 'temporary-public-storage',
    },
  },
};

Quelques points importants sur cette configuration :

Le choix des URLs est stratégique. Vous n'allez pas auditer 12 000 pages à chaque commit — un pipeline CI qui prend 45 minutes ne sera pas respecté par les développeurs. Sélectionnez une URL par template critique. La homepage, une page catégorie avec beaucoup de produits, une page produit, un article — c'est suffisant pour détecter 90% des régressions de performance liées au code.

Le numberOfRuns: 5 est un compromis entre fiabilité statistique et temps d'exécution. Lighthouse est notoirement variable d'un run à l'autre (±5-10% sur le score performance). LHCI prend la médiane des runs, ce qui lisse les outliers. En dessous de 3, vous aurez des faux positifs réguliers.

Assertions : la logique de seuils

Le système d'assertions est le cœur de LHCI. Chaque assertion accepte deux niveaux :

  • error : la CI échoue, le merge est bloqué
  • warn : un avertissement est loggé mais la CI passe

La stratégie recommandée : mettez en error les métriques qui ont un impact SEO direct mesurable (LCP, CLS, TBT comme proxy d'INP, et les audits SEO on-page). Mettez en warn les métriques secondaires ou les catégories où vos seuils sont encore en cours de calibration.

Pour aller plus loin sur la stratégie de seuils et de fréquence d'alerting, consultez cet article sur les seuils et fréquences d'alertes SEO.

Intégration dans les pipelines CI/CD

GitHub Actions

Voici un workflow GitHub Actions complet, testé en production :

# .github/workflows/lighthouse-ci.yml
name: Lighthouse CI
on:
  pull_request:
    branches: [main, staging]
  push:
    branches: [main]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build
        env:
          NODE_ENV: production

      - name: Run Lighthouse CI
        run: |
          npm install -g @lhci/[email protected]
          lhci autorun
        env:
          LHCI_BUILD_TOKEN: ${{ secrets.LHCI_BUILD_TOKEN }}
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}

      # Optionnel : upload les rapports HTML comme artifacts
      - name: Upload Lighthouse reports
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: lighthouse-reports
          path: .lighthouseci/
          retention-days: 14

La commande lhci autorun exécute séquentiellement : collectassertupload. Si les assertions échouent, le process exit avec un code non-zéro, ce qui fait échouer le step GitHub Actions et bloque le merge si vous avez activé les branch protection rules.

GitLab CI

# .gitlab-ci.yml
lighthouse:
  stage: test
  image: node:20-bullseye
  before_script:
    - apt-get update && apt-get install -y chromium
    - npm ci
    - npm install -g @lhci/[email protected]
  script:
    - npm run build
    - lhci autorun
  variables:
    CHROME_PATH: /usr/bin/chromium
    LHCI_BUILD_TOKEN: $LHCI_BUILD_TOKEN
  artifacts:
    paths:
      - .lighthouseci/
    expire_in: 2 weeks
  only:
    - merge_requests
    - main

Le point de friction le plus fréquent en CI : Chrome/Chromium qui ne se lance pas dans le conteneur. L'image node:20-bullseye combinée avec apt-get install chromium et le flag --no-sandbox dans la config Lighthouse résout 95% des cas. Si vous utilisez Alpine, passez à Debian — les dépendances de Chrome sur Alpine sont un enfer de configuration.

Parallélisation et temps d'exécution

Avec 5 URLs et 5 runs chacune, LHCI exécute 25 audits Lighthouse séquentiellement. Chaque audit prend ~15-30 secondes selon la complexité de la page. Comptez 6-12 minutes pour le job complet.

Pour réduire ce temps sur des projets à pipeline critique :

  • Réduisez à 3 runs (acceptable si votre CI est stable)
  • Limitez à onlyCategories: ['performance'] si les audits SEO et a11y sont couverts ailleurs
  • Exécutez LHCI uniquement sur les PRs qui touchent des fichiers frontend (paths filter dans le workflow)

C'est un trade-off entre couverture et feedback loop. Un pipeline de 12 minutes qui tourne à chaque commit est acceptable. Un pipeline de 30 minutes qui audite 20 URLs avec 7 runs ne sera bientôt plus exécuté que sur main.

Déployer le LHCI Server pour l'historique

Le temporary public storage fourni par Google est pratique pour démarrer, mais les données sont publiques et supprimées après 7 jours. Pour un monitoring continu réel, vous avez besoin du LHCI Server.

Docker Compose

# docker-compose.lhci.yml
version: '3.8'
services:
  lhci-server:
    image: patrickhulce/lhci-server:0.14.0
    ports:
      - '9001:9001'
    volumes:
      - lhci-data:/data
    environment:
      - LHCI_STORAGE__SQL_DIALECT=sqlite
      - LHCI_STORAGE__SQL_DATABASE_PATH=/data/lhci.db
      # Pour PostgreSQL en production :
      # - LHCI_STORAGE__SQL_DIALECT=postgres
      # - LHCI_STORAGE__SQL_CONNECTION_URL=postgresql://user:pass@db:5432/lhci
    restart: unless-stopped

volumes:
  lhci-data:

Après le déploiement, créez un projet et récupérez le build token :

# Création du projet sur le LHCI Server
lhci wizard --wizard=new-project --serverBaseUrl=https://lhci.votredomaine.com
# Output :
# Created project "mon-ecommerce" (id: xxxxx)
# Build token: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Admin token: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Use the build token in your CI config

Le dashboard LHCI Server est minimaliste mais fonctionnel. Il affiche les courbes de chaque métrique par URL au fil des builds, avec un diff visuel entre deux runs. C'est suffisant pour repérer une dégradation progressive du Speed Index sur trois semaines — le type de régression invisible dans un audit ponctuel.

Limites du LHCI Server

Le dashboard LHCI est centré sur la performance Lighthouse. Il ne corrèle pas avec vos données de trafic organique, ne surveille pas les changements de contenu (meta descriptions supprimées, canonicals cassées en production), et n'alerte pas en temps réel.

C'est un outil de gate CI, pas un outil de monitoring SEO complet. Pour la détection continue de régressions SEO en production — metas disparues, erreurs de SSR, backlinks perdus — un outil de monitoring dédié comme Seogard complète LHCI en couvrant ce que le pipeline CI ne voit pas : les problèmes qui surviennent après le déploiement, côté infrastructure ou côté données.

Scénario réel : migration SSR d'un e-commerce

Prenons un cas concret. Un e-commerce spécialisé outdoor — 15 000 pages produit, 800 pages catégories, 200 articles de blog — migre de Create React App (CSR pur) vers Next.js avec SSR. L'objectif : améliorer l'indexabilité et les Core Web Vitals.

Avant la migration

Les scores Lighthouse sur les pages clés (mesurés avec Chrome DevTools en émulation mobile) :

  • Homepage : Performance 42, LCP 5.8s, CLS 0.32
  • Page catégorie : Performance 28, LCP 7.1s (hydratation tardive du listing produit)
  • Page produit : Performance 35, LCP 6.4s (image hero chargée côté client)

Le rendu CSR causait des divergences majeures entre ce que Googlebot voyait et ce que les utilisateurs expérimentaient. Les pages étaient techniquement indexées, mais avec un contenu partiel — les données structurées Product étaient injectées côté client et irrégulièrement captées par le crawler.

Configuration LHCI pendant la migration

L'équipe configure LHCI dès le premier sprint de migration, avec des seuils progressifs :

Sprint 1-3 (fondations Next.js) : assertions en warn uniquement, seuils larges. L'objectif est de collecter une baseline.

Sprint 4-6 (optimisation SSR) : passage en error pour le LCP (< 3500ms), le CLS (< 0.15), et les audits SEO Lighthouse.

Sprint 7+ (fine-tuning) : seuils resserrés vers les objectifs finaux — LCP < 2500ms, CLS < 0.1, TBT < 300ms.

Le bug détecté par LHCI

Au sprint 5, un développeur refactore le composant de lazy loading des images produit. Le nouveau code utilise loading="lazy" sur l'image hero above the fold — une erreur classique qui empêche le navigateur de prioriser le LCP element.

// ❌ Code problématique détecté par LHCI
// Le composant ProductImage appliquait loading="lazy" à toutes les images
const ProductImage: React.FC<ProductImageProps> = ({ src, alt, priority }) => {
  return (
    <Image
      src={src}
      alt={alt}
      width={800}
      height={600}
      loading="lazy"     // ← Appliqué même à l'image hero (priority=true)
      decoding="async"
    />
  );
};

// ✅ Correction après l'alerte LHCI
const ProductImage: React.FC<ProductImageProps> = ({ src, alt, priority }) => {
  return (
    <Image
      src={src}
      alt={alt}
      width={800}
      height={600}
      loading={priority ? 'eager' : 'lazy'}
      decoding={priority ? 'sync' : 'async'}
      // Next.js Image avec priority={true} gère automatiquement le preload
      priority={priority}
      // fetchPriority pour les navigateurs qui le supportent
      fetchPriority={priority ? 'high' : 'auto'}
    />
  );
};

LHCI a bloqué le merge : le LCP de la page produit est passé de 2.1s à 3.8s entre la branche main et la PR. Le développeur a corrigé en 10 minutes. Sans LHCI, ce bug aurait été déployé un vendredi (comme c'est malheureusement souvent le cas) et serait resté en production jusqu'au prochain audit manuel.

Résultats post-migration

Après 8 sprints et ~200 builds LHCI :

  • LCP moyen pages produit : 6.4s → 1.9s
  • CLS moyen pages catégories : 0.32 → 0.04
  • Score performance Lighthouse : 28-42 → 78-91 selon le template
  • Pages indexées dans Search Console : +23% en 6 semaines (les pages auparavant rendues partiellement sont maintenant crawlées avec leur contenu complet)

Le trafic organique a augmenté de 31% sur les 3 mois suivant la fin de la migration — mais il est impossible d'attribuer ce gain uniquement aux Core Web Vitals. L'amélioration du SSR a aussi résolu des problèmes d'indexation du contenu. C'est la réalité du SEO technique : les optimisations s'empilent et leurs effets sont corrélés.

Stratégies avancées de configuration

Assertions budgétaires avec performance budgets

Au-delà des scores Lighthouse, LHCI supporte les performance budgets — des limites sur le poids des ressources :

// lighthouserc.js — section assert
assert: {
  assertions: {
    'resource-summary:script:size': ['error', { maxNumericValue: 350000 }],   // JS < 350KB
    'resource-summary:image:size': ['warn', { maxNumericValue: 500000 }],     // Images < 500KB
    'resource-summary:third-party:count': ['warn', { maxNumericValue: 8 }],   // Max 8 third-parties
    'resource-summary:total:size': ['error', { maxNumericValue: 1500000 }],   // Total < 1.5MB
    'unused-javascript': ['warn', { maxLength: 2 }],                          // Max 2 scripts inutilisés
  },
},

Ces budgets détectent une catégorie de régressions que les scores Lighthouse ne montrent pas toujours : un bundle JS qui grossit de 20KB à chaque sprint. Le score reste à 85, mais le bundle passe de 200KB à 450KB en 6 mois. Un jour, le score décroche brutalement — et il est trop tard pour identifier quel commit a ajouté les 250KB.

Audits conditionnels par type de page

Si votre site a des templates aux caractéristiques très différentes (pages produit avec beaucoup d'images vs. articles de blog textuels), des seuils uniques sont trop permissifs pour certains templates et trop stricts pour d'autres.

La solution : plusieurs configurations LHCI avec des overrides par URL pattern.

// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      url: [
        'http://localhost:3000/',
        'http://localhost:3000/c/chaussures-running',
        'http://localhost:3000/p/nike-pegasus-41-noir',
        'http://localhost:3000/blog/guide-pronation',
      ],
      numberOfRuns: 5,
      startServerCommand: 'npm run start',
    },
    assert: {
      assertions: {
        // Seuils par défaut
        'categories:performance': ['error', { minScore: 0.7 }],
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
      },
      assertMatrix: [
        {
          // Pages produit : LCP plus strict (image hero optimisée attendue)
          matchingUrlPattern: '.*/p/.*',
          assertions: {
            'largest-contentful-paint': ['error', { maxNumericValue: 2200 }],
            'cumulative-layout-shift': ['error', { maxNumericValue: 0.05 }],
          },
        },
        {
          // Pages catégorie : tolérance plus large (listings dynamiques)
          matchingUrlPattern: '.*/c/.*',
          assertions: {
            'largest-contentful-paint': ['error', { maxNumericValue: 2800 }],
            'total-blocking-time': ['error', { maxNumericValue: 400 }],
          },
        },
        {
          // Blog : performance stricte (pages légères)
          matchingUrlPattern: '.*/blog/.*',
          assertions: {
            'categories:performance': ['error', { minScore: 0.9 }],
            'largest-contentful-paint': ['error', { maxNumericValue: 1800 }],
          },
        },
      ],
    },
    upload: {
      target: 'lhci',
      serverBaseUrl: 'https://lhci.votredomaine.com',
      token: process.env.LHCI_BUILD_TOKEN,
    },
  },
};

L'assertMatrix est une fonctionnalité puissante mais peu documentée. Elle permet de définir des contrats de performance différents par template — exactement ce dont un site de 15K+ pages a besoin.

Intégration avec Screaming Frog et Search Console

LHCI couvre le monitoring pré-déploiement. Mais pour valider l'impact en production, croisez les données :

Search Console : le rapport Core Web Vitals (sous "Expérience") montre les données CrUX réelles. Après une amélioration détectée par LHCI en synthétique, vérifiez que les métriques terrain suivent dans les 28 jours suivants. Si LHCI montre un LCP à 2.0s mais CrUX reste à 3.5s, votre environnement CI ne reproduit pas les conditions réelles (serveur plus rapide, CDN absent, etc.). Consultez les rapports Search Console souvent négligés pour exploiter pleinement ces données.

Screaming Frog : pour auditer les URLs que LHCI ne couvre pas. Un crawl Screaming Frog de vos 15K pages avec l'intégration PageSpeed Insights activée prend ~4-6 heures (rate limited à ~2 requêtes/seconde par l'API PSI). Faites-le mensuellement pour identifier les templates ou pages spécifiques où la performance dévie de votre échantillon LHCI.

Chrome DevTools : pour le diagnostic post-régression. Quand LHCI détecte un LCP dégradé, utilisez le panel Performance de Chrome DevTools pour tracer l'origine exacte — est-ce le serveur (TTFB), le rendering, ou un script tiers ? Les techniques avancées de Chrome DevTools pour le SEO couvrent ces workflows en détail.

Les pièges courants et comment les éviter

Variance des résultats en CI

Le piège numéro un : des tests flaky qui échouent aléatoirement. Un runner CI partagé avec d'autres jobs, un réseau instable, un garbage collection Chrome mal tombé — et votre LCP varie de 800ms entre deux runs identiques.

Solutions concrètes :

  • 5 runs minimum avec prise de la médiane (pas la moyenne)
  • Runner dédié ou at minimum un runner avec des ressources CPU/RAM prévisibles
  • Seuils avec marge : si votre objectif réel est un LCP de 2.5s, mettez le seuil LHCI à 2.8s ou 3.0s pour absorber la variance synthétique
  • --chrome-flags="--deterministic-mode" : réduit la variance mais n'est pas supporté dans toutes les versions de Chrome

Tester contre localhost vs. staging

LHCI teste typiquement un serveur local (startServerCommand). Cela signifie : pas de CDN, pas de latence réseau réelle, pas de charge concurrente. Votre LCP local à 1.2s deviendra 2.4s en production derrière un CDN mal configuré ou avec des tiers qui ajoutent 500ms.

Pour les projets matures, ajoutez un second job LHCI qui teste contre l'environnement de staging après déploiement :

// lighthouserc.staging.js
module.exports = {
  ci: {
    collect: {
      url: [
        'https://staging.votredomaine.com/',
        'https://staging.votredomaine.com/c/chaussures-running',
        'https://staging.votredomaine.com/p/nike-pegasus-41-noir',
      ],
      numberOfRuns: 3,
      // Pas de startServerCommand — on teste un environnement distant
    },
    // ... assertions et upload identiques
  },
};

C'est plus réaliste, mais plus lent et dépendant de l'infrastructure staging. Le meilleur setup combine les deux : LHCI local en gate sur les PR (feedback rapide), LHCI staging post-deploy sur main (validation réaliste).

Ne pas auditer les bonnes pages

Si vous n'auditez que la homepage, vous manquerez la régression du composant de listing produit qui affecte 800 pages catégories. Inversement, auditer 50 URLs différentes ralentit le pipeline au point de devenir inutile.

La règle : une URL par template, pondérée par le trafic. Identifiez dans Search Console vos 4-5 templates qui génèrent 80% du trafic organique. Choisissez l'URL la plus représentative de chaque template (ni la plus légère, ni la plus lourde — la médiane).

Complémentarité avec le monitoring continu

LHCI est un garde-fou pré-déploiement. Il répond à la question : "est-ce que ce changement de code dégrade la performance ?" Mais il ne répond pas à : "est-ce que la performance en production est stable cette semaine ?"

Les régressions qui échappent à LHCI :

  • Un tiers (analytics, A/B testing, consent manager) qui ralentit après une mise à jour côté provider
  • Un CDN qui expire un cache et force des recalculs serveur
  • Une base de données qui ralentit sous la charge et dégrade le TTFB
  • Un changement de configuration Nginx/Cloudflare fait par l'ops sans passer par le pipeline CI

Ces régressions surviennent en production, sans changement de code. Les audits ponctuels ne les détectent pas — seul un monitoring continu en production le peut.

L'approche robuste combine trois couches : LHCI dans le CI/CD pour les régressions code, des données RUM (Real User Monitoring) pour les performances terrain, et un pipeline d'automatisation SEO dans le CI/CD qui couvre les aspects non-performance (metas, canonicals, robots, structured data).

LHCI est un outil de développeur, pas un outil de monitoring SEO. Il fait très bien son job — bloquer les régressions de performance au niveau du code — mais il ne remplace ni la Search Console, ni Screaming Frog, ni un monitoring de production continu. Utilisez-le comme première ligne de défense, et complétez avec des outils qui surveillent ce qui se passe réellement sur vos pages en production, 24/7.