Skip to content

Next Performance & Optimization

Next.js ships fast by default. Then you add images , custom fonts , and third-party scripts and suddenly your Lighthouse score looks like a 2010 WordPress site

Performance in Next.js is about configuring what the framework already provides. Image optimization , font loading , ISR tuning , and CDN caching are built in - you just need to enable them correctly. One unoptimized image can undo every caching strategy you've implemented

image optimization

The next/image component replaces HTML <img> tags. It's not optional

import Image from 'next/image'

export default function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero image"
      width={1200}
      height={600}
      priority // above-the-fold images get priority loading
      placeholder="blur" // show blur-up placeholder while loading
      blurDataURL="data:image/jpeg;base64,..." // tiny blurred version
    />
  )
}

What next/image does:

  • Automatic WebP/AVIF conversion - serves modern formats when the browser supports them
  • Responsive sizing - generates multiple sizes , serves the right one per viewport
  • Lazy loading - images load when they scroll into viewport (except priority ones)
  • CLS prevention - reserves space to prevent layout shift
// Remote images need domain allowlist
// next.config.ts
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
        pathname: '/**'
      },
      {
        protocol: 'https',
        hostname: 'cdn.example.com',
        pathname: '/**'
      }
    ]
  }
}

Without remotePatterns , remote images fail with a 400 error. Every image domain must be explicitly allowed

// Optimize images from remote sources
<Image
  src="https://images.unsplash.com/photo-123"
  alt="Unsplash image"
  width={800}
  height={600}
  sizes="(max-width: 768px) 100vw, 50vw" // responsive sizes
/>

The sizes attribute tells Next.js which image size to generate. Wrong sizes means wrong image dimensions shipped to the wrong device

font optimization

Custom fonts block rendering. Next.js's next/font eliminates that

// src/app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // show fallback font immediately , swap when custom loads
  variable: '--font-inter' // CSS variable for use in styles
})

const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-mono'
})

export default function RootLayout({
  children
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
      <body>{children}</body>
    </html>
  )
}
/* Use the CSS variables in your styles */
body {
  font-family: var(--font-inter);
}

code {
  font-family: var(--font-mono);
}

For self-hosted fonts:

import localFont from 'next/font/local'

const myFont = localFont({
  src: './fonts/my-custom-font.woff2',
  display: 'swap',
  variable: '--font-custom'
})

next/font downloads font files at build time and serves them from your domain. No external requests to Google Fonts. No render-blocking font loading

bundle analysis

See exactly what's in your JavaScript bundle

npm install @next/bundle-analyzer
// next.config.ts
import type { NextConfig } from 'next'

const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true'
})

const nextConfig: NextConfig = {
  // your config
}

export default withBundleAnalyzer(nextConfig)
ANALYZE=true npm run build
# Opens a visual report of your bundle in the browser

What to look for:

  • Large dependencies in client bundles - moment.js (500kb) , lodash (200kb) → use tree-shakeable alternatives
  • Server-only code in client - if you see Prisma , database drivers , or server utils in the client bundle , you're leaking
  • Duplicate libraries - multiple versions of the same library from different dependencies
  • Heavy Client Components - components marked 'use client' that import large libraries

ISR tuning

ISR is powerful but configuration matters

// Revalidate too often - database hammering
fetch('https://api.example.com/posts', {
  next: { revalidate: 1 } // revalidates every second - why even use ISR?
})

// Revalidate too rarely - stale content
fetch('https://api.example.com/posts', {
  next: { revalidate: 86400 } // 24 hours - users see stale data all day
})

// Smart revalidation - match content update frequency
fetch('https://api.example.com/posts', {
  next: {
    revalidate: 300, // 5 minutes - good balance for blog content
    tags: ['posts']  // allows on-demand revalidation
  }
})

On-demand revalidation via webhooks is the best pattern:

// src/app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache'

export async function POST(request: Request) {
  const secret = request.headers.get('x-revalidation-secret')

  if (secret !== process.env.REVALIDATION_SECRET) {
    return Response.json({ error: 'Invalid secret' }, { status: 401 })
  }

  const { tag } = await request.json()
  revalidateTag(tag)

  return Response.json({ revalidated: true })
}

Trigger revalidation from your CMS or database:

curl -X POST https://your-app.com/api/revalidate \
  -H "x-revalidation-secret: your-secret" \
  -H "Content-Type: application/json" \
  -d '{"tag": "posts"}'

edge caching

Next.js caches aggressively. Understanding the cache layers helps you tune performance

Full Route Cache (server-side): * Caches rendered HTML for static and ISR pages * Invalidated by revalidatePath() or revalidateTag() * On Vercel , cached at the edge

Data Cache (fetch responses): * Caches fetch() responses * Controlled by cache and next.revalidate options * Invalidated by revalidateTag() or time-based expiry

Router Cache (client-side): * Caches page payloads in the browser * Prefetched pages stored for 30 seconds (static) or 5 minutes (full navigation) * Invalidated by router.refresh()

// Force cache miss for fresh data
fetch('https://api.example.com/data', {
  cache: 'no-store' // bypasses all caching
})

// Cache with tag for later invalidation
fetch('https://api.example.com/data', {
  next: {
    tags: ['dashboard-data'],
    revalidate: 900 // 15 minutes
  }
})

CDN strategies

Next.js static assets should be served from a CDN

// next.config.ts
const nextConfig = {
  // Prefix for CDN - upload .next/static to your CDN
  assetPrefix: process.env.CDN_URL || undefined,

  // Set Cache-Control headers for static assets
  async headers() {
    return [
      {
        source: '/:all*(svg|png|jpg|jpeg|gif|webp|woff|woff2)',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable'
          }
        ]
      }
    ]
  }
}

On Vercel , CDN caching is automatic. The static files in _next/static are served with immutable cache headers and distributed across Vercel's edge network

For self-hosted deployments:

# Nginx configuration for Next.js static assets
location /_next/static {
    alias /var/www/myapp/.next/static;
    expires 365d;
    add_header Cache-Control "public, immutable";
}

location / {
    proxy_pass http://localhost:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
}

performance checklist

  • Images use next/image with explicit width/height - no oversized images
  • Above-fold images have priority attribute - no lazy-loading critical content
  • Fonts use next/font with display: swap - no FOIT (flash of invisible text)
  • ISR pages have sensible revalidate intervals based on content update frequency
  • Bundle analyzed - no server code in client bundles , no oversized dependencies
  • router.refresh() used after mutations instead of full page reload
  • Scripts use next/script with appropriate strategy (beforeInteractive , afterInteractive , lazyOnLoad)
  • Static assets have immutable cache headers

prerequisites

next_13_deploy.md - you should understand deployment options and environment configuration


next → (end of section)