The Problem with "Lighthouse 100" Claims
Most tutorials show you how to score 100 on a blank Next.js page with one . That's useless. Real apps have Google Analytics, Intercom, custom fonts, hero images above the fold, and 30 npm packages.
This guide covers what we actually do at NineLab to hit 95–100 on production client sites.
The Three Killers of Lighthouse Score
1. Largest Contentful Paint (LCP) — Target: < 2.5s
LCP measures when the largest visible element renders. Almost always, this is your hero image.
Fix: Preload the hero image
// app/layout.tsx
export default function Layout({ children }) {
return (
<html>
<head>
<link
rel="preload"
as="image"
href="/hero.jpg"
fetchPriority="high"
/>
</head>
<body>{children}</body>
</html>
)
}
Fix: Use priority on the hero Image
<Image
src="/hero.jpg"
alt="Hero"
fill
priority // This adds fetchpriority="high" + preload link
sizes="100vw"
/>
Fix: Avoid lazy loading above-the-fold images
// ❌ Wrong — never lazy load above-the-fold
<Image src="/hero.jpg" loading="lazy" />
// ✅ Correct
<Image src="/hero.jpg" priority />
2. Cumulative Layout Shift (CLS) — Target: < 0.1
CLS happens when elements move after initial render. Common culprits:
Fix: Always specify image dimensions
// ✅ Fixed dimensions prevent layout shift
<Image src="/logo.png" width={200} height={60} alt="Logo" />
// Or use aspect-ratio for responsive images
<div style={{ aspectRatio: '16/9', position: 'relative' }}>
<Image src="/cover.jpg" fill alt="Cover" />
</div>
Fix: Font display swap
const inter = Inter({
subsets: ['latin'],
display: 'swap', // Show fallback font immediately, swap when loaded
})
3. Third-Party Scripts
Google Tag Manager alone can cost you 15–20 Lighthouse points if loaded synchronously.
Fix: Use Next.js Script component
import Script from 'next/script'
// afterInteractive = loads after page is interactive
<Script
src="https://www.googletagmanager.com/gtag/js"
strategy="afterInteractive"
/>
// lazyOnload = lowest priority, loads when browser is idle
<Script
src="https://widget.intercom.io/widget/xxx"
strategy="lazyOnload"
/>
Advanced: Bundle Analysis
npm install @next/bundle-analyzer
// next.config.mjs
import bundleAnalyzer from '@next/bundle-analyzer'
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
})
export default withBundleAnalyzer({})
ANALYZE=true npm run build
Look for any single module over 50KB. Those are your targets.
The Checklist We Use at NineLab
Following this checklist consistently gets us 95+ on every project.
