Next.js static export et Core Web Vitals : le playbook 2026
Pour atteindre 95+ PageSpeed mobile sur Next.js static export : self-host les fonts via raw @font-face (pas next/font/local), preload la font LCP avec fetchPriority=high, éviter Framer Motion sur above-the-fold, utiliser uniquement transform/opacity pour les animations, réserver l'espace layout pour le contenu i18n, servir via Cloudflare free tier.
Après 12 mois de données production sur 40+ sites Next.js construits en static export (output: 'export'), voici le playbook exact qu'on applique pour atteindre 95+ PageSpeed mobile. Pas de théorie — que ce qui marche mesurablement.
Baseline : ce que static export donne gratuitement
Avec output: 'export' + Vercel ou hébergeur standard, un site Next.js vanilla score déjà bien car tout le HTML est pré-rendu, TTFB faible, pas de cold start, code splitting automatique par route. Un site Next.js 15 static export vierge score 95-100 mobile. Dès qu'on ajoute Framer Motion, Google Fonts, icônes, analytics, on tombe à 70-85 sans s'en rendre compte.
LCP : la bataille contre le render-blocking CSS
Les rapports PageSpeed sur nos sites Next.js montrent systématiquement le même problème : deux fichiers CSS bloquants (un Tailwind, un next/font Google déclarations) qui ajoutent ~500ms de render delay sur mobile throttled. C'est 100% du budget LCP.
1. Supprimer le CSS render-blocking des fonts
Le plus gros gain : remplacer next/font/google pour la font display par une déclaration @font-face raw pointant une URL stable dans public/fonts/.
@font-face {
font-family: 'Space Grotesk';
font-style: normal;
font-weight: 500 700;
font-display: swap;
src: url('/fonts/space-grotesk-latin.woff2') format('woff2');
size-adjust: 102%;
ascent-override: 95%;
}Stratégie font qui marche vraiment
Notre stack standard 2026 :
- Display font (headings) — 1 variable font chargée via raw @font-face, range 500-700, preload fetchPriority=high. Typique : Space Grotesk, Inter, Geist.
- Body font — next/font/google Inter avec 2 weights (400, 600), subset latin, adjustFontFallback=Arial. Auto-preload par Next.js.
- Monospace (code blocks) — next/font/google JetBrains Mono, 1 weight (400), preload=false.
INP : pièges Framer Motion à éviter
INP a remplacé FID en mars 2024 et est bien plus strict. Au-dessus de 200ms, Google considère 'poor'. Framer Motion est souvent le coupable.
Piège 1 : framer-motion au mount initial
// ❌ Mauvais: délai LCP
<motion.h1 initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}>
Notre titre
</motion.h1>
// ✅ Bon: CSS keyframe, zéro délai JS
<h1 className="ow-anim-fade-up">Notre titre</h1>Piège 2 : animer des propriétés layout
Ne jamais animer width, height, top, left, margin. Chaque frame force un reflow complet. Utiliser transform et opacity exclusivement — compositor-only, pas de layout.
Piège 3 : trop d'animations simultanées
Règle : 1-2 animations max par viewport visible. Utiliser whileInView avec once: true.
Piège 4 : pas de useReducedMotion
Le hook useReducedMotion de Framer Motion permet de skip les animations si prefers-reduced-motion est set. Accessibilité ET performance.
CLS : le pattern de réservation i18n
Sur sites i18n, le CLS apparaît quand les traductions chargent après le render initial (react-i18next TranslationProvider hydrate côté client). Un container vide au first paint se remplit soudainement avec 3-4 lignes, poussant 80-120px.
<div
className="min-h-[280px] sm:min-h-[220px] md:min-h-[160px] lg:min-h-[130px]"
data-speakable="true"
>
<p>{t('tldr.content')}</p>
</div>Images : AVIF, priority, fetchPriority
images: {
unoptimized: true, // obligatoire pour output: 'export'
formats: ['image/avif', 'image/webp'],
}En static export, unoptimized: true est obligatoire. Il faut pré-générer les variants AVIF + WebP au build via un script sharp.
Hosting : Brotli 11, HTTP/3, Early Hints
Peu importe l'optimisation du code, le hosting détermine les 10-15 derniers points de PageSpeed. Stack recommandée 2026 :
- Host origin — n'importe quel host statique (Cloudways, Netlify, Vercel, Cloudflare Pages, S3).
- Cloudflare free tier en front — HTTP/3 QUIC, Brotli 11, edge cache, protection DDoS, 30 min de setup.
- Early Hints (103) activés — dashboard Cloudflare. Envoie preload hints avant réponse origin — élimine le wait TTFB du critical path.
- Cache rule —
_next/static/*→ Cache Everything, Edge TTL 1 an. Hashed names = immutable-safe. - Purge cache on deploy — wrangler pages deployment ou Cloudflare API dans CI.
Comment débugger une régression PageSpeed
Méthode pour identifier la cause en 10 minutes :
- Comparer rapports PageSpeed — ouvrir last-known-good et régressé côte à côte. Noter quelle métrique a drop.
- Check critical request chain — voir ce qui bloque le render qui ne bloquait pas avant.
- Chrome DevTools Performance — localhost avec throttling 4x CPU + Fast 3G. Timeline main thread.
- Git diff deps — ajout npm package ? Packages lourds (Framer Motion, lodash, moment) ajoutent First Load JS.
- Check next build output — comparer route sizes avec last deploy. Route +20KB = suspect.
