Skip to main content
Back to Blog
Next.jsReactPerformanceWeb DevelopmentTypeScript

Next.js Performance Optimization: A Complete 2025 Guide

Master Next.js performance with proven techniques for Core Web Vitals, image optimization, caching strategies, and build-time improvements. Real-world tips from shipping production apps.

5 min read

After building and optimizing several production Next.js applications including this portfolio and InsureSignal, I've compiled the most impactful performance techniques that actually move the needle on Core Web Vitals and user experience.

This guide assumes you're comfortable with TypeScript and React state management.

Why Performance Matters

Google uses Core Web Vitals as a ranking factor. Beyond SEO, fast sites convert better:

  • 53% of mobile users abandon sites that take over 3 seconds to load
  • Every 100ms of latency costs Amazon 1% in sales
  • Pinterest increased search traffic by 15% after reducing wait time by 40%

Core Web Vitals Deep Dive

Largest Contentful Paint (LCP)

Target: Under 2.5 seconds

Key optimizations:

// Use Next.js Image component with priority for hero images
import Image from 'next/image';

export function Hero() {
  return (
    <Image
      src="/hero.webp"
      alt="Hero image"
      width={1200}
      height={600}
      priority  // Preloads the image
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,..."
    />
  );
}

Font optimization:

// next.config.js - Enable font optimization
const nextConfig = {
  experimental: {
    optimizePackageImports: ['@heroicons/react'],
  },
};

Use next/font with display: swap to prevent FOIT:

import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  preload: true,
});

First Input Delay (FID) / Interaction to Next Paint (INP)

Target: Under 100ms for FID, under 200ms for INP

Reduce JavaScript bundle size:

# Analyze your bundle
npm install @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({
  // config
});

Code split aggressively:

// Dynamic imports for non-critical components
import dynamic from 'next/dynamic';

const HeavyChart = dynamic(() => import('@/components/Chart'), {
  loading: () => <ChartSkeleton />,
  ssr: false,
});

Cumulative Layout Shift (CLS)

Target: Under 0.1

Always specify dimensions:

// Bad - causes layout shift
<img src="/photo.jpg" alt="Photo" />

// Good - reserves space
<Image
  src="/photo.jpg"
  alt="Photo"
  width={800}
  height={600}
/>

Use CSS aspect-ratio:

.video-container {
  aspect-ratio: 16 / 9;
  width: 100%;
}

Caching Strategies

Static Generation (SSG)

For content that rarely changes:

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

Incremental Static Regeneration (ISR)

Best of both worlds - static with updates:

export const revalidate = 3600; // Revalidate every hour

export default async function Page() {
  const data = await fetch('https://api.example.com/data');
  return <Component data={data} />;
}

HTTP Caching Headers

// next.config.js
async headers() {
  return [
    {
      source: '/_next/static/:path*',
      headers: [
        {
          key: 'Cache-Control',
          value: 'public, max-age=31536000, immutable',
        },
      ],
    },
  ];
}

Image Optimization

Next.js Image component is powerful but needs proper configuration:

// next.config.js
const nextConfig = {
  images: {
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920],
    imageSizes: [16, 32, 48, 64, 96, 128, 256],
    minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
  },
};

Responsive images done right:

<Image
  src="/hero.jpg"
  alt="Hero"
  fill
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  className="object-cover"
/>

Build Optimization

Reduce Build Times

// next.config.js
const nextConfig = {
  // Skip type checking during build (do it in CI)
  typescript: {
    ignoreBuildErrors: process.env.CI !== 'true',
  },
  // Parallel routes compilation
  experimental: {
    parallelServerCompiles: true,
    parallelServerBuildTraces: true,
  },
};

Tree Shaking

Import only what you need:

// Bad - imports entire library
import * as Icons from 'lucide-react';

// Good - tree-shakeable
import { ArrowRight, Check } from 'lucide-react';

Server Components Best Practices

Next.js 13+ App Router defaults to Server Components:

// This runs on the server - no JS sent to client
async function ProductList() {
  const products = await db.products.findMany();

  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

Only use "use client" when needed:

  • Event handlers (onClick, onChange)
  • Browser APIs (localStorage, window)
  • React hooks (useState, useEffect)
  • Third-party client libraries

Monitoring Performance

Vercel Analytics

// app/layout.tsx
import { Analytics } from '@vercel/analytics/react';
import { SpeedInsights } from '@vercel/speed-insights/next';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Analytics />
        <SpeedInsights />
      </body>
    </html>
  );
}

Lighthouse CI

Set up automated performance testing in your CI pipeline:

# .github/workflows/lighthouse.yml
- name: Lighthouse
  uses: treosh/lighthouse-ci-action@v10
  with:
    budgetPath: ./budget.json
    uploadArtifacts: true

Real Results

On my portfolio site, these optimizations achieved:

  • LCP: 1.2s (from 3.4s)
  • FID: 12ms (from 89ms)
  • CLS: 0.02 (from 0.18)
  • Lighthouse Score: 98 (from 72)

Performance optimization is iterative. Measure, optimize, deploy, repeat. The techniques above have worked across multiple production applications I've built.

For more Next.js projects, check out my portfolio where you can see these optimizations in action.


Related Articles: