Next.js Performance Optimization: Crushing Core Web Vitals
Advanced techniques for optimizing Next.js apps: image optimization, code splitting, caching strategies, and achieving perfect Lighthouse scores.
Next.js Performance Optimization: Crushing Core Web Vitals
Google now ranks by performance metrics, not just content. Here's how to get green scores.
Understanding Core Web Vitals
Three metrics Google cares about:
1. LCP (Largest Contentful Paint): Largest visible element renders. Target: <2.5s
2. FID (First Input Delay): Time from user input to JS response. Target: <100ms
3. CLS (Cumulative Layout Shift): How much does layout move? Target: <0.1
Poor scores = lower rankings = less traffic = missed revenue.
Image Optimization: The Biggest Win
Use Next.js Image Component
Instead of:
<img src="/hero.jpg" alt="Hero" /> // BAD: no optimization
// BAD: no optimization
Do this:Do this:
import Image from "next/image";
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={630}
priority // Only for LCP images
sizes="(max-width: 640px) 100vw, 50vw"
/>import Image from "next/image";
src="/hero.jpg" alt="Hero" width={1200} height={630} priority // Only for LCP images sizes="(max-width: 640px) 100vw, 50vw" /> Next.js automatically: WebP is 25-35% smaller than JPEG. AVIF is another 20% smaller. Let the browser choose: Or let Next.js handle it with `priority` and `sizes` attributes. Don't load code users won't see: import dynamic from "next/dynamic"; const HeavyDashboard = dynamic(() => import("./dashboard"), { loading: }); export default function Home() { return } With Next.js App Router, every page is an automatic code split. A user on /contact doesn't download /blog code. Split large components: const Modal = dynamic(() => import("./modal"), { ssr: false, // Don't render on server }); System fonts load instantly. Google Fonts? Not so much. // next.config.ts const nextConfig = { optimizeFonts: true, }; Better: use `display: swap` to show fallback while loading: @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap"); Or self-host fonts (eliminates Google network request): npm install @next/font import { Inter } from "@next/font/google"; const inter = Inter({ subsets: ["latin"] }); export default function RootLayout() { return ( ... ); } Build-time HTML. Perfect for marketing pages: export const revalidate = 86400; // Revalidate once per day export default function About() { return
Next.js automatically:
- Resizes for different screen sizes
- Converts to WebP (30% smaller)
- Lazy loads below-the-fold
- Prevents layout shift (fixed aspect ratio)
### Image Formats Matter
WebP is 25-35% smaller than JPEG. AVIF is another 20% smaller.
Let the browser choose:Image Formats Matter
<picture>
<source srcSet="/image.avif" type="image/avif" />
<source srcSet="/image.webp" type="image/webp" />
<img src="/image.jpg" alt="Fallback" />
</picture>
Or let Next.js handle it with `priority` and `sizes` attributes.
## Code Splitting & Lazy Loading
### Dynamic Imports
Don't load code users won't see:
Code Splitting & Lazy Loading
Dynamic Imports
import dynamic from "next/dynamic";
const HeavyDashboard = dynamic(() => import("./dashboard"), {
loading: <Skeleton />
});
export default function Home() {
return <HeavyDashboard />; // Only loads when Home renders
}
### Route-Based Code Splitting
With Next.js App Router, every page is an automatic code split. A user on /contact doesn't download /blog code.
### Component-Level Splitting
Split large components:
Route-Based Code Splitting
Component-Level Splitting
const Modal = dynamic(() => import("./modal"), {
ssr: false, // Don't render on server
});
## Fonts: Often Overlooked
System fonts load instantly. Google Fonts? Not so much.
Fonts: Often Overlooked
// next.config.ts
const nextConfig = {
optimizeFonts: true,
};
Better: use `display: swap` to show fallback while loading:
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap");
Or self-host fonts (eliminates Google network request):
npm install @next/fontimport { Inter } from "@next/font/google";
const inter = Inter({ subsets: ["latin"] });
export default function RootLayout() {
return (
<html className={inter.className}>
...
</html>
);
}
## Caching Strategy
### Static Generation (Fastest)
Build-time HTML. Perfect for marketing pages:
Caching Strategy
Static Generation (Fastest)
export const revalidate = 86400; // Revalidate once per day
export default function About() {
return <div>About Page</div>;
}
}
### ISR (Incremental Static Regeneration)
Revalidate in background:
ISR (Incremental Static Regeneration)
Revalidate in background:
export const revalidate = 3600; // Revalidate every hour
export default function Blog({ params }: { params: { slug: string } }) {
const post = getPost(params.slug);
return <article>{post.content}</article>;
}export const revalidate = 3600; // Revalidate every hour
export default function Blog({ params }: { params: { slug: string } }) {
const post = getPost(params.slug);
return
}
Google crawls static pages. They rank better.
### Server Components (When Content Changes)
Use server components for fresh data:
Google crawls static pages. They rank better.
Server Components (When Content Changes)
Use server components for fresh data:
// No "use client" = server component
export default async function Dashboard() {
const data = await fetch("https://api.example.com/data");
return <div>{data}</div>;
}// No "use client" = server component
export default async function Dashboard() {
const data = await fetch("https://api.example.com/data");
return
}
## JavaScript Bloat
### Third-Party Scripts
Scripts like analytics and ads murder performance. Load them asynchronously:
JavaScript Bloat
Third-Party Scripts
Scripts like analytics and ads murder performance. Load them asynchronously:
<Script
src="https://cdn.example.com/analytics.js"
strategy="lazyOnload"
/>