A slow Next.js app is almost always caused by the same 6 things.
Here's how to find and fix all of them.
1. Images — The Biggest Performance Win
Images are responsible for most slow page loads.
// 🚫 Regular img — no optimization
<img src="/hero.jpg" alt="Hero" className="w-full" />
// ✅ Next.js Image — automatic WebP, lazy load, size optimization
import Image from 'next/image';
// For images above the fold — add priority
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // preloads — only use for above-fold images
className="w-full"
/>
// For images below the fold — lazy load by default
<Image
src="/feature.jpg"
alt="Feature"
width={600}
height={400}
// No priority — lazy loads automatically
/>
Additional tips:
- Use WebP format for all images
- Compress images before uploading — use squoosh.app
- Never use
fillwithout a sized parent container
2. Fonts — Eliminate Layout Shift
// 🚫 External font link — causes layout shift and render blocking
<link href="https://fonts.googleapis.com/css2?family=Inter" rel="stylesheet" />
// ✅ next/font — self-hosted, zero layout shift
import { Inter, Plus_Jakarta_Sans } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap', // show fallback font while loading
variable: '--font-inter',
});
const jakarta = Plus_Jakarta_Sans({
subsets: ['latin'],
display: 'swap',
variable: '--font-jakarta',
});
export default function RootLayout({ children }) {
return (
<html className={`${inter.variable} ${jakarta.variable}`}>
<body>{children}</body>
</html>
);
}
3. Bundle Size — Stop Loading What You Don't Need
Check your bundle size:
npm install -D @next/bundle-analyzer
# next.config.ts
import bundleAnalyzer from '@next/bundle-analyzer';
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});
export default withBundleAnalyzer({ /* your config */ });
# Run analysis
ANALYZE=true npm run build
Common fixes:
// 🚫 Importing entire library
import _ from 'lodash';
const result = _.groupBy(items, 'category');
// ✅ Import only what you need
import groupBy from 'lodash/groupBy';
const result = groupBy(items, 'category');
// 🚫 Loading heavy component on every page
import { HeavyChart } from '@/components/HeavyChart';
// ✅ Dynamic import — only loads when needed
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
loading: () => <div className="animate-pulse h-64 bg-slate-700 rounded" />,
ssr: false, // skip server render for client-only components
});
4. Server Components — Reduce JavaScript Sent to Browser
Every 'use client' component adds JavaScript to your bundle.
// 🚫 Whole page is client JavaScript
'use client';
export default function Dashboard() {
const [data, setData] = useState(null);
// fetches on client, adds to bundle
}
// ✅ Server component — zero client JavaScript
export default async function Dashboard() {
const data = await db.analytics.findMany();
// fetches on server, sends only HTML
return <AnalyticsDisplay data={data} />;
}
// Only make interactive parts client components
'use client';
export function AnalyticsDisplay({ data }) {
const [view, setView] = useState('chart');
// minimal client JS
}
Rule: only add 'use client' when you need useState, useEffect, or browser APIs.
5. Suspense and Streaming
Don't let one slow query block the whole page:
import { Suspense } from 'react';
export default function Dashboard() {
return (
<div>
{/* Shows instantly */}
<DashboardHeader />
{/* Streams in when ready */}
<Suspense fallback={<StatsSkeleton />}>
<SlowStats /> {/* fetches its own data */}
</Suspense>
{/* Streams in independently */}
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart /> {/* fetches its own data */}
</Suspense>
</div>
);
}
Each Suspense boundary streams independently. User sees content progressively instead of waiting for everything.
6. Metadata and Preloading
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<head>
{/* Preconnect to external domains */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
{/* DNS prefetch for API domains */}
<link rel="dns-prefetch" href="https://api.example.com" />
{/* Preload critical assets */}
<link
rel="preload"
href="/fonts/inter.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
</head>
<body>{children}</body>
</html>
);
}
Performance Checklist
Run through this before every deployment:
Images:
- [ ] All images use
next/image - [ ] Above-fold images have
priority - [ ] Images compressed to WebP
Fonts:
- [ ] Using
next/fontnot external links - [ ]
display: 'swap'on all fonts
JavaScript:
- [ ] Heavy components use dynamic imports
- [ ] Third party libraries imported selectively
- [ ] Minimal
'use client'usage
Data fetching:
- [ ] Slow queries wrapped in Suspense
- [ ] Static data cached appropriately
- [ ] No waterfall fetches (fetch in parallel)
Core Web Vitals targets:
- LCP under 2.5 seconds
- FID under 100ms
- CLS under 0.1
Measure First
Don't optimize blindly. Measure first:
# Lighthouse in terminal
npx lighthouse https://yoursite.com --view
# Or use PageSpeed Insights
# https://pagespeed.web.dev/
Fix what Lighthouse flags before anything else. It tells you exactly what's slow and how to fix it.
I applied all these optimizations to my Next.js templates — they all score 95+ on Lighthouse:
- https://neurodash-dashbord.vercel.app/
- https://og-ai-next.vercel.app/
- https://agencyx-template.vercel.app/
Get the templates: https://pixelanas.gumroad.com
What's your biggest Next.js performance win? Drop it below 👇
Anas — full-stack Next.js developer. X: @pixelanas
Top comments (0)