Complete Next.js Performance & SEO Optimization Guide
Source: Dev.to
Built‑in Component Optimization
Next.js provides optimized built‑in components that automatically handle performance best practices, eliminating the need for manual optimization and reducing common pitfalls.
next/image
The Image component is one of the most powerful performance tools in Next.js. It provides:
- Automatic lazy loading
- Image resizing based on device
- Format optimization (WebP, AVIF)
- Prevention of Cumulative Layout Shift (CLS)
- Reduced bandwidth usage
Basic implementation
import Image from 'next/image';
export default function ProductCard() {
return (
);
}
Advanced configuration (next.config.js)
module.exports = {
images: {
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
domains: ['example.com', 'cdn.example.com'], // Allowed external domains
remotePatterns: [
{
protocol: 'https',
hostname: '**.example.com',
pathname: '/images/**',
},
],
formats: ['image/webp', 'image/avif'],
minimumCacheTTL: 60,
dangerouslyAllowSVG: true,
contentSecurityPolicy:
"default-src 'self'; script-src 'none'; sandbox;",
},
};
Points to remember
- Use
priority={true}for above‑the‑fold images. - Always specify
widthandheightto prevent layout shift. - Use the
fillprop for responsive containers. - External images require domain whitelisting.
- WebP format provides ~30 % smaller file sizes than JPEG.
next/link
Enables client‑side navigation without a full page reload.
import Link from 'next/link';
export default function Navigation() {
return (
About
Blog
);
}
Points to remember
- Set
prefetch={false}on links to content that may not be visited.
Script Loading Strategies
Optimizing third‑party script loading prevents blocking the main thread and improves page‑load performance. The Script component gives fine‑grained control over when and how scripts are loaded.
Critical scripts – beforeInteractive
Loads before any Next.js code and before page hydration. Use for scripts that must run before user interaction.
import Script from 'next/script';
export default function RootLayout({ children }) {
return (
{children}
);
}
Analytics & non‑critical scripts – afterInteractive
Loads after the page becomes interactive. Ideal for analytics, ads, and other non‑essential scripts.
import Script from 'next/script';
export default function Analytics() {
return (
console.log('Analytics loaded')}
/>
);
}
Lazy‑load scripts – lazyOnload
Loads during browser idle time. Perfect for chat widgets, social media buttons, or any non‑essential scripts.
import Script from 'next/script';
export default function ChatWidget() {
return (
console.log('Chat widget ready')}
/>
);
}
Worker strategy – worker
Runs scripts in a web worker for better performance.
import Script from 'next/script';
export default function Page() {
return (
);
}
Points to remember
- Use
beforeInteractiveonly for truly critical scripts. - Most analytics should use
afterInteractive. - Social media widgets and chat should use
lazyOnload. - Prefer
onLoad/onReadycallbacks over inline scripts.
Dependency Management
Removing unused dependencies reduces bundle size, improves build times, and minimizes security vulnerabilities.
Identify unused packages
# Install depcheck globally
npm install -g depcheck # or: yarn global add depcheck
# Run in project root
depcheck
# With options
depcheck --ignores="eslint-*,@types/*"
# Specify directories
depcheck ./src --ignore-dirs=build,dist
Example output
Unused dependencies
* lodash
* moment
* axios
Missing dependencies
* react-icons
Unused devDependencies
* @testing-library/jest-dom
Automated cleanup script (package.json)
{
"scripts": {
"deps:check": "depcheck",
"deps:clean": "depcheck --json | jq -r '.dependencies[]' | xargs npm uninstall"
}
}
Points to remember
- Run
depcheckregularly (monthly or before major releases). - Verify packages used only in config files before removal.
- Types packages (
@types/*) may not appear in the search; keep them if needed. - Test thoroughly after removing dependencies.
- Keep
devDependenciesseparate fromdependencies. - Document why a seemingly unused package is retained.
Caching and Incremental Static Regeneration (ISR)
Smart caching strategies serve content faster, reduce server load, and keep data fresh. ISR lets you update static pages after build time.
Basic ISR implementation
// app/blog/[slug]/page.js
export const revalidate = 3600; // Revalidate every hour
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map(post => ({ slug: post.slug }));
}
export default async function BlogPost({ params }) {
const post = await getPost(params.slug);
return (
## {post.title}
{post.content}
{post.publishedAt}
);
}
On‑Demand Revalidation
// app/api/revalidate/route.js
import { revalidatePath } from 'next/cache';
import { NextResponse } from 'next/server';
export async function POST(request) {
const secret = request.nextUrl.searchParams.get('secret');
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ message: 'Invalid secret' }, { status: 401 });
}
const path = request.nextUrl.searchParams.get('path');
if (path) {
revalidatePath(path);
return NextResponse.json({ revalidated: true, now: Date.now() });
}
return NextResponse.json({ message: 'Missing path' }, { status: 400 });
}
Tag‑based Revalidation
// Fetch with cache tags
export default async function ProductList() {
const products = await fetch('https://api.example.com/products', {
next: { tags: ['products'], revalidate: 3600 }
});
return {/* Render products */};
}
// app/api/revalidate-products/route.js
import { revalidateTag } from 'next/cache';
import { NextResponse } from 'next/server';
export async function POST() {
revalidateTag('products');
return NextResponse.json({ revalidated: true });
}
Cache‑Control Headers
// app/api/data/route.js
export async function GET() {
const data = await fetchData();
return NextResponse.json(data, {
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300'
}
});
}
Points to remember
- ISR is ideal for content that updates periodically (blogs, product listings).
- Use shorter revalidation intervals (e.g., 60 s) for frequently changing data.
- Combine ISR with tag‑based revalidation for fine‑grained cache invalidation.
Font Optimization
Next.js automatically optimizes fonts when using the built‑in next/font API.
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
weight: ['400', '700'],
variable: '--font-inter',
});
export default function RootLayout({ children }) {
return (
{children}
);
}
Tips
- Load only the character subsets you need.
- Prefer variable fonts to reduce the number of requests.
- Avoid
@importin CSS; use thenext/fontAPI instead.
Lazy Loading & Code Splitting
Dynamic imports
import dynamic from 'next/dynamic';
const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), {
loading: () =>
Loading…
,
ssr: false, // Load only on client side
});
- Split large libraries (e.g., charting, maps) into separate chunks.
- Use
ssr: falsefor components that rely on browser APIs.
List virtualization
For long lists, use libraries like react-window or react-virtualized to render only visible items.
import { FixedSizeList as List } from 'react-window';
export default function VirtualizedList({ items }) {
return (
{({ index, style }) => (
{items[index].title}
)}
);
}
Data Fetching Optimization
Client‑side fetching with SWR
import useSWR from 'swr';
const fetcher = url => fetch(url).then(res => res.json());
export default function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher, {
revalidateOnFocus: false,
dedupingInterval: 60000,
});
if (isLoading) return
Loading…
;
if (error) return
Error loading profile.
;
return {data.name};
}
- Set
revalidateOnFocustofalsefor data that doesn’t need frequent updates. - Adjust
dedupingIntervalto control request deduplication.
JavaScript Bundle Optimization
- Tree‑shaking: Ensure you import only needed functions (
import { foo } from 'lodash'instead ofimport _ from 'lodash'). - Analyze bundles: Use
next build && next analyzeor tools likewebpack-bundle-analyzer. - Compress assets: Enable gzip/Brotli on the CDN or Vercel edge.
CSS Optimization
- Use CSS modules or styled‑components to scope styles and avoid global bloat.
- Remove unused CSS with tools like
purgecss(integrated in Next.js vianext-purgecss). - Prefer
@mediaqueries over separate CSS files for responsive design.
API Route Caching
Cache expensive API responses at the edge.
// app/api/products/route.js
import { NextResponse } from 'next/server';
import { fetchProducts } from '@/lib/db';
export async function GET() {
const products = await fetchProducts();
return NextResponse.json(products, {
headers: {
'Cache-Control': 'public, s-maxage=120, stale-while-revalidate=300',
},
});
}
Edge Runtime
Deploy compute‑intensive logic to the Edge Runtime for low latency.
// app/api/geo/route.js
export const runtime = 'edge';
export async function GET(request) {
const ip = request.headers.get('x-forwarded-for') ?? 'unknown';
// Perform fast geo lookup
return new Response(`Your IP: ${ip}`);
}
Core Web Vitals
- Largest Contentful Paint (LCP): Keep LCP low by optimizing above‑the‑fold content.
export default function BlogPost({ post }) {
return (
{post.title} – My Blog
{/* Post content */}
);
}
Security Optimization
- Set
Content‑Security‑Policy,X‑Content‑Type‑Options, andReferrer‑Policyheaders in API routes or via a middleware. - Use
next-safeor similar libraries to sanitize user‑generated HTML. - Keep dependencies up to date; run
npm auditregularly.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const response = NextResponse.next();
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; img-src https: data:; script-src 'self' 'unsafe-inline';"
);
return response;
}