← Back to Blog

Next.js Performance Optimization: Core Web Vitals and Beyond

A slow Next.js app is a missed opportunity. Here are the optimization strategies that actually move the needle on LCP, INP, and CLS.

Web developer optimizing application performance

Next.js ships with strong performance defaults, but defaults only take you so far. Real-world applications accumulate third-party scripts, large images, bloated bundles, and render-blocking patterns that erode the speed advantages you started with. This guide covers the high-impact optimization strategies for Next.js — with particular focus on Core Web Vitals, which directly influence SEO rankings and user retention.

Understanding Core Web Vitals in 2026

Google's Core Web Vitals are the performance metrics that matter most for search ranking and user experience. In 2024, INP (Interaction to Next Paint) replaced FID (First Input Delay) as the interactivity metric. The current set:

  • LCP (Largest Contentful Paint): Time until the largest visible element loads. Target: under 2.5 seconds. This is almost always the hero image or above-the-fold heading.
  • INP (Interaction to Next Paint): How quickly the page responds to user interactions throughout the entire visit. Target: under 200ms. Replaced FID in March 2024.
  • CLS (Cumulative Layout Shift): Visual stability — how much the layout shifts unexpectedly. Target: under 0.1. Caused by images without dimensions, late-loading fonts, and dynamic content injection.

For more on measuring these, see our guide on Core Web Vitals 2026: Measurement and Improvement.

Rendering Strategy Selection

Next.js gives you multiple rendering modes. Choosing the right one per page is the most impactful performance decision you'll make:

Static Generation (SSG) — The Default Target

Pages built at compile time, served from CDN. Fastest possible TTFB (Time to First Byte) because there's no server processing. Use for: marketing pages, blog posts, documentation, product listings that don't change per-request.

// app/blog/[slug]/page.tsx
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 <article>{post.content}</article>;
}

Incremental Static Regeneration (ISR)

Static pages that regenerate in the background after a defined interval. The sweet spot for content that changes periodically: product pages, news articles, event listings. Use revalidate to control freshness:

export const revalidate = 3600; // Revalidate every hour

export default async function ProductPage({ params }) {
  const product = await getProduct(params.id);
  return <ProductView product={product} />;
}

Server-Side Rendering (SSR)

Generated on each request. Use only when the page content must be fresh per-request AND personalized (dashboard, cart, authenticated pages). SSR has higher TTFB — don't use it for static content just because it feels safer.

Image Optimization

Images are the #1 cause of poor LCP scores. Next.js's Image component handles most of this automatically — but only if you use it correctly:

import Image from 'next/image';

// Good: next/image handles format conversion, lazy loading, sizing
<Image
  src="/hero.jpg"
  alt="Hero banner"
  width={1200}
  height={600}
  priority  // Add for above-the-fold images — disables lazy loading
  sizes="(max-width: 768px) 100vw, 1200px"
/>

// Bad: native img tag bypasses all optimization
<img src="/hero.jpg" alt="Hero banner" />

Key Image Rules

  • Add priority to your LCP image (the first visible hero image) — this prevents lazy loading the most important asset
  • Always specify sizes for responsive images to prevent downloading unnecessarily large images on mobile
  • Use WebP or AVIF — Next.js converts automatically via the formats config in next.config.js
  • Avoid fill layout on LCP images — it requires additional CSS positioning that can delay render

Bundle Analysis and Code Splitting

A bloated JavaScript bundle is the primary cause of poor INP scores. Measure first:

# Install bundle analyzer
npm install @next/bundle-analyzer

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

module.exports = withBundleAnalyzer({});

# Run analysis
ANALYZE=true npm run build

Common Bundle Bloat Causes

  • Moment.js: 67KB gzipped — replace with date-fns or Temporal API
  • lodash: Import specific functions, not the whole library (import debounce from 'lodash/debounce' not import _ from 'lodash')
  • Large icon libraries: Import individual icons, not entire sets
  • Unoptimized third-party scripts: Use Next.js Script component with appropriate strategy

Dynamic Imports for Non-Critical Components

import dynamic from 'next/dynamic';

// Heavy components load only when needed
const HeavyChart = dynamic(() => import('../components/HeavyChart'), {
  loading: () => <ChartSkeleton />,
  ssr: false, // Skip server render for client-only components
});

// Modal — no point loading until user triggers it
const ContactModal = dynamic(() => import('../components/ContactModal'));

Font Optimization

Unoptimized fonts cause both LCP delays (render-blocking) and CLS (layout shifts when fonts swap). Next.js Font handles this correctly:

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

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',  // Prevents invisible text during font load
  preload: true,
});

export default function RootLayout({ children }) {
  return (
    <html className={inter.className}>
      <body>{children}</body>
    </html>
  );
}

Next.js Font downloads fonts at build time and serves them from your domain — eliminating third-party DNS lookups and preloading fonts automatically for zero CLS from font swaps.

Third-Party Script Management

Analytics, chat widgets, and ad scripts are among the most common performance killers. Next.js's Script component gives you control:

import Script from 'next/script';

// afterInteractive: load after page is interactive (analytics)
<Script src="https://analytics.example.com/script.js" strategy="afterInteractive" />

// lazyOnload: load during idle time (chat widgets, social embeds)
<Script src="https://chat.example.com/widget.js" strategy="lazyOnload" />

// beforeInteractive: critical scripts only (rarely needed)
<Script src="/critical-polyfill.js" strategy="beforeInteractive" />

Caching Strategy

Next.js App Router has a layered caching system. Understanding it prevents unintentional stale data:

  • Request Memoization: Identical fetch() calls in the same render are deduplicated automatically
  • Data Cache: Persists across requests. Control with { cache: 'no-store' } for real-time data or { next: { revalidate: 3600 } } for ISR-like behavior
  • Full Route Cache: Static routes cached at build time on the server
  • Router Cache: Client-side cache of previously visited routes

Frequently Asked Questions

What's the fastest way to improve LCP on an existing Next.js site?

Identify your LCP element using Chrome DevTools or PageSpeed Insights. It's almost always an image or large text block. If it's an image: switch to next/image with priority, ensure it has proper sizes, and check that it's not loading from a slow origin. If it's text: check for render-blocking CSS or fonts delaying paint.

Should I use the Pages Router or App Router for a performance-sensitive application?

App Router (Next.js 13+) with React Server Components provides better performance for data-heavy pages by eliminating unnecessary client-side JavaScript. For new projects, App Router is the better choice. Migration from Pages Router is incremental — you can mix both in the same project.

How do I reduce CLS from dynamically loaded content?

Reserve space for dynamic content before it loads using CSS aspect-ratio or explicit height/width. For ads or embeds with variable height, use a minimum height wrapper. For skeleton loading states, match the skeleton dimensions to the loaded content dimensions precisely.

What tools should I use to measure performance?

Lighthouse (Chrome DevTools or CLI) for synthetic testing. PageSpeed Insights for real-world field data. Vercel Analytics or SpeedCurve for continuous monitoring. Chrome User Experience Report (CrUX) for aggregate real-user data over time.

Related Reading

Need help optimizing your Next.js application?

We audit and optimize Next.js applications for Core Web Vitals, SEO performance, and user experience — from bundle analysis to rendering strategy review.

Get a Performance Audit