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.
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é :
- Migration vers Next.js 16 + Server Components partout (1 jour)
- Image hero : conversion AVIF + priority + blur (2h)
- Fonts : Geist via next/font + swap (1h)
- 3rd party (GTM, Hotjar) en lazyOnload (2h)
- Suppression de Framer Motion sur la home (remplacé par CSS animations) (3h)
- Tailwind 4 + critical CSS inline (1h)
- 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+blurplaceholder - [ ] Fonts via
next/font(ou équivalent avec swap) - [ ] Bundle JS < 100 KB sur landing
- [ ] 3rd party en
lazyOnloadou 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