Back to Blog
Development
June 5, 2025
6 min read
1,123 words

Optimizing Performance in Next.js Applications

Technical guide to improving load times and Core Web Vitals in your Next.js web applications.

Optimizing Performance in Next.js Applications

Performance Is a Feature

I have audited dozens of Next.js applications, and the pattern is consistent: teams launch with excellent Core Web Vitals, then watch performance degrade as features accumulate. Performance is not a one-time optimization—it requires ongoing attention and measurement.

In this guide, I will share the techniques that consistently improve Next.js performance, from quick wins to advanced optimizations.

Understanding Core Web Vitals

The Three Metrics That Matter

  • LCP (Largest Contentful Paint): Time for the main content to load. Target: under 2.5s.
  • FID (First Input Delay): Time until the page responds to interaction. Target: under 100ms.
  • CLS (Cumulative Layout Shift): Visual stability—content should not jump around. Target: under 0.1.

Why They Matter

  • Google uses Core Web Vitals as ranking signals
  • Slow sites lose users: 53% abandon if load takes over 3 seconds
  • Performance directly impacts conversion rates

Image Optimization

The next/image Component

Images are typically the largest elements on a page. Next.js provides automatic optimization:

// Always use next/image for automatic optimization
import Image from 'next/image';

export function Hero() {
  return (
    
Hero image
); } // For dynamic images (e.g., from CMS) export function ProductImage({ src, alt }) { return ( {alt} ); }

Image Best Practices

  • Use priority: For above-the-fold images that impact LCP.
  • Specify sizes: Helps browser choose the right image size.
  • Use blur placeholders: Improves perceived performance.
  • Avoid layout shift: Always provide width and height or use fill.

Code Splitting

Dynamic Imports

Split large components to reduce initial bundle size:

import dynamic from 'next/dynamic';

// Heavy component loaded only when needed
const HeavyChart = dynamic(
  () => import('@/components/HeavyChart'),
  {
    loading: () => ,
    ssr: false // Client-only if needed
  }
);

// Modal loaded on user interaction
const [showModal, setShowModal] = useState(false);
const Modal = dynamic(() => import('@/components/Modal'));

// Usage: Modal only loads when showModal is true
{showModal &&  setShowModal(false)} />}

Route-Based Splitting

Next.js automatically splits by route. Help it by:

  • Keeping page components focused
  • Moving shared code to layout.tsx
  • Using barrel exports carefully (they can bloat bundles)

Server Components and Client Components

Default to Server Components

In Next.js 13+, components are Server Components by default. This means:

  • Zero JavaScript sent to client for static rendering
  • Direct database/API access without client exposure
  • Faster initial page load

When to Use Client Components

Add 'use client' only when you need:

  • useState, useEffect, or other hooks
  • Event handlers (onClick, onChange)
  • Browser-only APIs (localStorage, window)
  • Third-party libraries requiring browser environment
// Server Component (default) - no JavaScript to client
export default async function ProductPage({ params }) {
  const product = await getProduct(params.id);
  
  return (
    

{product.name}

{product.description}

{/* Client boundary */}
); } // Client Component - only interactive part 'use client' export function AddToCartButton({ productId }) { const [added, setAdded] = useState(false); return ( ); }

Caching and Data Fetching

Next.js Fetch Caching

// Cached by default (can be served from CDN)
const data = await fetch('https://api.example.com/data');

// Revalidate every hour
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 3600 }
});

// Fresh on every request
const data = await fetch('https://api.example.com/data', {
  cache: 'no-store'
});

// On-demand revalidation
import { revalidateTag } from 'next/cache';
// In API route after mutation:
revalidateTag('products');

Static Generation (SSG) vs Server Rendering (SSR)

  • SSG: Build-time rendering. Best for content that rarely changes.
  • ISR: Static with background revalidation. Best for frequent but not real-time updates.
  • SSR: Request-time rendering. For personalized or highly dynamic content.

Font Optimization

Using next/font

Fonts can block rendering. Next.js provides automatic optimization:

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

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // Prevents FOIT
  variable: '--font-inter',
});

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

export default function RootLayout({ children }) {
  return (
    
      {children}
    
  );
}

Font Best Practices

  • Use variable fonts to reduce file count
  • Limit font weights and subsets
  • Use font-display: swap to prevent invisible text
  • Self-host fonts via next/font for best performance

Script Optimization

Using next/script

import Script from 'next/script';

// Analytics - loads after page is interactive