Lenobot
Retour au blog

Optimisation performance web : passer en mode Lighthouse 100 en 2026

Lighthouse 100/100 en 2026, c'est jouable même sur des sites complexes. Voici notre méthode pas à pas avec INP, LCP, et tous les détails qui comptent.

6 avril 20269 min de lecture
Optimisation performance web : passer en mode Lighthouse 100 en 2026

Lighthouse 100/100 sur mobile, en 2026, sur un site avec contenu réel ? Oui c'est possible. On l'a fait sur 11 projets clients ces 12 derniers mois. Voici notre méthode complète, sans bullshit, avec les vraies métriques qui comptent en 2026 (spoiler : INP a remplacé FID, et c'est plus dur).

Les Core Web Vitals en 2026 (rappel)

  • LCP (Largest Contentful Paint) : <= 2.5s. Le plus gros élément visible doit s'afficher vite.
  • INP (Interaction to Next Paint) : <= 200ms. Toute interaction doit donner un retour visuel rapide. INP a remplacé FID en 2024.
  • CLS (Cumulative Layout Shift) : <= 0.1. Pas de saute pendant le chargement.

Lighthouse mesure aussi FCP (First Contentful Paint), TBT (Total Blocking Time), Speed Index. Et accessibilité, SEO, best practices à 100% si vous êtes sérieux.

Étape 1 : LCP < 1.5s

Identifier le LCP

Dans Chrome DevTools > Performance > recording. L'élément LCP est annoté.

C'est généralement :

  • Une image hero
  • Un titre H1 large
  • Un block de texte au-dessus du fold

Optimiser une image hero

// Next.js 16
import Image from 'next/image';
import heroImage from '@/public/hero.jpg';

export default function Hero() {
  return (
    <section className="relative h-[600px]">
      <Image
        src={heroImage}
        alt="Notre service"
        fill
        priority // CRITICAL : preload
        sizes="100vw"
        placeholder="blur" // blur instantané
        className="object-cover"
        quality={85} // 85 = sweet spot qualité/taille
      />
      <h1 className="absolute inset-0 flex items-center justify-center text-5xl text-white">
        Bienvenue
      </h1>
    </section>
  );
}

Les 3 mots magiques : priority, sizes, placeholder="blur".

Preload la font hero

// app/layout.tsx
import { GeistSans } from 'geist/font/sans';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="fr" className={GeistSans.variable}>
      <body>{children}</body>
    </html>
  );
}

Next/font fait le preload + fallback automatiquement. Pas besoin de <link preload> manuel.

Server Components partout

Un RSC envoie du HTML direct. Pas de waterfall fetch côté client. Votre LCP est dans le HTML initial, c'est le mode hard.

// app/page.tsx (RSC)
import { db } from '@/db';

export default async function HomePage() {
  const featured = await db.query.products.findFirst({
    where: (p, { eq }) => eq(p.featured, true),
  });

  return (
    <main>
      <Hero image={featured.image} title={featured.title} />
    </main>
  );
}

LCP < 800ms en général sur Hetzner Postgres + edge cache.

Étape 2 : INP < 100ms

INP est la métrique la plus dure en 2026. Elle mesure la réactivité de chaque interaction (click, tap, keypress) sur toute la session, pas juste la première.

Causes principales de mauvais INP

  • JS bloquant le main thread
  • Re-renders React massifs
  • Synchronous layout dans event handlers
  • 3rd party scripts qui font tout exploser

Stratégie 1 : moins de JS client

La meilleure JS, c'est celle qu'on n'envoie pas. Server Components + Server Actions vous économisent 50-90% de bundle.

# Audit du bundle
ANALYZE=true pnpm build

Objectif : < 100 KB JS sur la home (gzip).

Stratégie 2 : useTransition pour les updates lourdes

'use client';
import { useTransition, useState } from 'react';

export function FilterableList({ allItems }: Props) {
  const [filter, setFilter] = useState('');
  const [isPending, startTransition] = useTransition();

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    const value = e.target.value;
    startTransition(() => setFilter(value)); // non bloquant
  }

  const filtered = allItems.filter(item => item.name.includes(filter));

  return (
    <>
      <input onChange={handleChange} />
      {isPending && <Spinner />}
      <ul style={{ opacity: isPending ? 0.5 : 1 }}>
        {filtered.map(i => <li key={i.id}>{i.name}</li>)}
      </ul>
    </>
  );
}

L'input reste réactif même si la liste prend 500ms à recompute.

Stratégie 3 : déférer les 3rd party

GTM, Hotjar, intercom : tueurs d'INP. Chargez-les après l'idle :

'use client';
import Script from 'next/script';

export function Analytics() {
  return (
    <Script
      src="https://www.googletagmanager.com/gtag/js?id=GA-XXX"
      strategy="lazyOnload"
    />
  );
}

Ou mieux : Partytown pour exécuter les 3rd party dans un Web Worker.

pnpm add @builder.io/partytown
<Script type="text/partytown" src="https://...gtag.js" />

Le script tourne dans un worker, le main thread reste libre.

Étape 3 : CLS = 0

La promesse zéro layout shift demande de la discipline.

Toujours dimensionner les images

// Next/Image avec width/height OU fill + parent dimensionné
<Image src={img} alt="" width={1200} height={800} />

// OU
<div className="relative aspect-[3/2]">
  <Image src={img} alt="" fill />
</div>

Réservez l'espace pour les contenus dynamiques

<div className="min-h-[200px]">
  <Suspense fallback={<Skeleton className="h-[200px]" />}>
    <DynamicChart />
  </Suspense>
</div>

Fonts : font-display swap + size-adjust

Next/font gère ça automatiquement avec les metrics fallback. Pas de FOIT, pas de CLS.

Étape 4 : optimiser le critical CSS

Tailwind 4 fait du purge agressif natif. Notre output CSS sur un site complet : 18 KB gzip. Inline si vous voulez la perfection :

// next.config.ts
export default {
  experimental: {
    inlineCss: true, // Next.js 16, expérimental mais marche bien
  },
};

Étape 5 : caching et CDN

Static-first avec ISR

// app/blog/[slug]/page.tsx
export const revalidate = 3600; // 1h ISR

export async function generateStaticParams() {
  const posts = await db.query.posts.findMany({ columns: { slug: true } });
  return posts.map((p) => ({ slug: p.slug }));
}

Le contenu est servi statique depuis le CDN, régénéré toutes les heures. TTFB < 50ms global.

Cache headers personnalisés

// middleware.ts
import { NextResponse } from 'next/server';

export function middleware(request: Request) {
  const response = NextResponse.next();
  const url = new URL(request.url);

  if (url.pathname.startsWith('/_next/static/')) {
    response.headers.set('cache-control', 'public, max-age=31536000, immutable');
  } else if (url.pathname.startsWith('/api/')) {
    response.headers.set('cache-control', 'no-store');
  }

  return response;
}

Cloudflare devant tout

Proxy Cloudflare en mode "Cache Everything" pour les assets statiques + HTML. Notre stack :

  • HTML : cache 1h sur Cloudflare, stale-while-revalidate 24h
  • Static assets : cache 1 an immutable
  • API : pas de cache

💡 Vous voulez qu'on fasse passer votre site à Lighthouse 100 pour vous ? On en discute 15 minutes : rdv.lenobot.com.

Étape 6 : préchargement intelligent

import Link from 'next/link';

<Link href="/products" prefetch={true}>
  Catalogue
</Link>

Next.js prefetch les pages au hover/focus du link, par défaut. Quand l'user clique, la nav est instantanée.

Pour les routes critiques :

<Link href="/checkout" prefetch={true} prefetchOnView={true}>
  Passer commande
</Link>

Étape 7 : audit final et chasse aux derniers ms

Real User Monitoring

Lighthouse en lab c'est bien, mais le vrai monde c'est différent. Installez un RUM :

// app/layout.tsx
import Script from 'next/script';

<Script id="web-vitals">{`
  import('https://unpkg.com/web-vitals@4?module').then(({ onCLS, onLCP, onINP }) => {
    const send = (metric) => {
      navigator.sendBeacon('/api/vitals', JSON.stringify(metric));
    };
    onCLS(send);
    onLCP(send);
    onINP(send);
  });
`}</Script>

Mieux : utilisez Vercel Analytics ou Cloudflare Web Analytics qui le font sans config.

Tester sur du vrai matériel

Lighthouse simule un Moto G4. Mais testez aussi :

  • Vrai smartphone moyen de gamme (Samsung Galaxy A33)
  • 4G slow throttled
  • DevTools CPU x4 slowdown

C'est sur ces conditions que les bugs INP apparaissent.

Cas client : passage de 67 à 100 en 5 jours

Client SaaS B2B, site Next.js 14, score Lighthouse mobile 67. Métriques avant :

  • LCP : 4.2s
  • INP : 380ms
  • CLS : 0.18

Le diff appliqué :

  1. Migration vers Next.js 16 + Server Components partout (1 jour)
  2. Image hero : conversion AVIF + priority + blur (2h)
  3. Fonts : Geist via next/font + swap (1h)
  4. 3rd party (GTM, Hotjar) en lazyOnload (2h)
  5. Suppression de Framer Motion sur la home (remplacé par CSS animations) (3h)
  6. Tailwind 4 + critical CSS inline (1h)
  7. Cloudflare cache HTML + static assets (1h)

Métriques après :

  • LCP : 0.8s
  • INP : 80ms
  • CLS : 0.02
  • Lighthouse mobile : 100/100

Conversion +18% sur le trimestre suivant. La perf paie.

La checklist Lighthouse 100

  • [ ] Next.js 16 ou équivalent moderne
  • [ ] Server Components / SSR par défaut
  • [ ] Image hero avec priority + blur placeholder
  • [ ] Fonts via next/font (ou équivalent avec swap)
  • [ ] Bundle JS < 100 KB sur landing
  • [ ] 3rd party en lazyOnload ou Partytown
  • [ ] Tailwind 4 ou CSS purged
  • [ ] Static / ISR pour les pages contenu
  • [ ] CDN devant tout
  • [ ] Toutes les images dimensionnées
  • [ ] Pas de layout shift sur les content dynamiques
  • [ ] RUM installé pour mesurer le vrai monde
  • [ ] Test sur vrai mobile + slow 4G

Si vous cochez tout, vous êtes à 95+. Pour les 5 derniers points, c'est de la chasse aux ms.

Prêt à passer votre site à Lighthouse 100 sur votre stack en 2026 ? Notre équipe de devs seniors vous accompagne. Réservez votre appel découverte gratuit sur rdv.lenobot.com, 15 minutes pour évaluer votre projet, devis ferme sous 48h, sans engagement.

Article rédigé par L'équipe Lenobot.

Besoin d'aide avec votre projet ?

Nos experts sont prêts à vous accompagner dans votre transformation digitale.

Discutons de votre projet

Articles similaires

Lighthouse 100/100 en 2026 : guide perf web complet | Lenobot