Complete Next.js Performance & SEO Optimization Guide

Published: (December 15, 2025 at 02:27 PM EST)
7 min read
Source: Dev.to

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 width and height to prevent layout shift.
  • Use the fill prop for responsive containers.
  • External images require domain whitelisting.
  • WebP format provides ~30 % smaller file sizes than JPEG.

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 beforeInteractive only for truly critical scripts.
  • Most analytics should use afterInteractive.
  • Social media widgets and chat should use lazyOnload.
  • Prefer onLoad / onReady callbacks 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 depcheck regularly (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 devDependencies separate from dependencies.
  • 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 @import in CSS; use the next/font API 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: false for 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 revalidateOnFocus to false for data that doesn’t need frequent updates.
  • Adjust dedupingInterval to control request deduplication.

JavaScript Bundle Optimization

  • Tree‑shaking: Ensure you import only needed functions (import { foo } from 'lodash' instead of import _ from 'lodash').
  • Analyze bundles: Use next build && next analyze or tools like webpack-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 via next-purgecss).
  • Prefer @media queries 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, and Referrer‑Policy headers in API routes or via a middleware.
  • Use next-safe or similar libraries to sanitize user‑generated HTML.
  • Keep dependencies up to date; run npm audit regularly.
// 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;
}
Back to Blog

Related posts

Read more »