Speed up your Next.js app with image optimization, code splitting, caching headers, bundle analysis, and rendering strategy choices.
Core Web Vitals directly affect your Google search ranking. Slow sites lose users — 53% of mobile visitors abandon a page that takes over 3 seconds to load. Next.js ships with powerful optimization tools, but they only work if you use them correctly.
Here are 10 techniques that make the biggest real-world difference.
The <Image> component automatically:
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero banner"
width={1200}
height={600}
priority // add for above-the-fold images
quality={85}
/>
Never use <img> directly in Next.js.
// ISR — revalidate every 60 seconds
export const revalidate = 60;
// Force dynamic (server per request)
export const dynamic = 'force-dynamic';
npm install -D @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({});
ANALYZE=true npm run build
This opens a visual tree map of your bundle. Common culprits: moment.js (500KB), lodash (70KB), large icon libraries.
Don't load code until it's needed:
import dynamic from 'next/dynamic';
const RichTextEditor = dynamic(() => import('./RichTextEditor'), {
loading: () => <div className="h-64 bg-gray-100 animate-pulse rounded" />,
ssr: false, // disable SSR for client-only components
});
Self-host fonts for zero layout shift and no external requests:
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
});
export default function RootLayout({ children }) {
return (
<html className={inter.className}>
{children}
</html>
);
}
// app/api/products/route.ts
export async function GET() {
const products = await getProducts();
return Response.json(products, {
headers: {
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
},
});
}
Fetch data directly in server components — no extra API roundtrip from the client:
// This component runs on the server — no bundle cost
async function ProductList() {
const products = await db.product.findMany(); // direct DB access
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
Mark components with 'use client' only when they actually need browser APIs, state, or event handlers. Everything else should be a server component.
// In your root layout
<link rel="preload" href="/fonts/inter.woff2" as="font" crossOrigin="anonymous" />
<link rel="dns-prefetch" href="https://api.example.com" />
<link rel="preconnect" href="https://api.example.com" />
// next.config.js
module.exports = {
experimental: {
ppr: true,
},
};
PPR lets you have a static shell with dynamic "holes" — the best of static and dynamic in the same page.
Always measure before and after:
npx lighthouse https://your-site.com --view
Target scores: LCP < 2.5s, FID < 100ms, CLS < 0.1.
Start with image optimization and rendering strategy — these two changes alone typically move your Lighthouse score by 20-30 points. Then analyze your bundle, defer what you can, and measure again. Performance is iterative, not a one-time fix.