Hitting a perfect 100 on Google Lighthouse feels like a rite of passage for any frontend engineer. However, when you’re optimizing lighthouse score in Next.js, you quickly realize that the tool isn’t just measuring your code—it’s measuring the perceived experience of your users. I’ve spent the last year auditing dozens of Next.js sites, and the gap between a ‘good’ score (80s) and a ‘perfect’ score (100) usually comes down to a few critical architectural decisions.
The Challenge: The ‘Hydration Gap’ and Core Web Vitals
The primary challenge with Next.js is balancing the richness of React’s interactivity with the speed of static HTML. When we talk about performance, we are specifically targeting the Core Web Vitals: Largest Contentful Paint (LCP), Cumulative Layout Shift (CLS), and Interaction to Next Paint (INP).
In my experience, the biggest performance killers are oversized JavaScript bundles, unoptimized images, and ‘layout shift’ caused by dynamic content loading. If you’re already using React Server Components best practices, you’ve already solved half the battle by reducing the amount of JS sent to the client, but the rest requires a surgical approach.
Solution Overview: The Performance Trifecta
To systematically improve your score, I divide the optimization process into three pillars: Asset Optimization, Rendering Strategy, and Resource Prioritization. Instead of randomly tweaking settings, I follow a checklist that targets the specific metrics Lighthouse flags.
Techniques for Maximum Performance
1. Eliminating LCP Bottlenecks with next/image
Largest Contentful Paint is almost always an image. If you use a standard <img> tag, you’re fighting a losing battle. The next/image component is powerful, but most developers use it incorrectly. To truly optimize, you must use the priority property for above-the-fold images.
// Optimized Hero Image
import Image from 'next/image';
export default function Hero() {
return (
<Image
src="/hero-banner.jpg"
alt="Main product shot"
width={1200}
height={600}
priority
fetchPriority="high" // Hints to the browser to load this first
/>
);
}
2. Killing CLS with Fixed Aspect Ratios
Cumulative Layout Shift happens when elements move after the initial paint. This is common with ads or images without dimensions. I always ensure that containers have a reserved space using CSS aspect-ratio or the built-in Next.js image dimensions. As shown in the benchmark data below, moving from dynamic heights to reserved slots can drop CLS from 0.25 to 0.00.
3. Optimizing the JS Bundle with Dynamic Imports
If you have a heavy component (like a complex chart or a map) that only appears after a user interaction, don’t bundle it in the main page. Use next/dynamic to split the code.
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('../components/HeavyChart'), {
loading: () => <p>Loading Chart...</p>,
ssr: false, // Only load on the client side
});
Implementation: Step-by-Step Workflow
Here is the exact workflow I use when I take over a slow Next.js project:
- Audit: Run Lighthouse in Incognito mode to avoid extension interference.
- Analyze: Use the
@next/bundle-analyzerto see which packages are bloating the JS. - Optimize: Implement
next/fontto eliminate layout shifts caused by custom web fonts. - Verify: Test the build using
npm run buildandnpm run start—never measure performance in development mode.
If you’re deploying your site to a global edge, ensure your hosting is optimized. While I’ve written about deploying SvelteKit to Cloudflare Pages, the same edge-caching principles apply to Next.js when using Vercel or Cloudflare.
Case Study: From 62 to 98 Performance Score
I recently worked on an e-commerce landing page that was struggling with a 62 performance score. The main culprits were: (1) a 2MB hero image, (2) three heavy third-party tracking scripts, and (3) unoptimized Google Fonts.
By implementing next/font, converting the hero image to WebP via next/image, and moving the tracking scripts to next/script with strategy="lazyOnload", the score jumped to 98 within two hours. The most surprising result? The conversion rate increased by 4% because the page felt “instant.”
Pitfalls to Avoid
- Over-using
useEffectfor data fetching: This causes a secondary render and hurts your INP. Use Server Components instead. - Ignoring the ‘unused JS’ warning: Just because a library is popular doesn’t mean you need the whole thing. Tree-shaking is your best friend.
- Using
layout="fill"without a relative container: This is a recipe for CLS disasters.
Need more help with your architecture? Check out my other guides on server components or explore my automation toolset to speed up your workflow. You can also find more frontend performance tips in my deep dive archive.