Performance Optimization in Next.js: Real Metrics, Real Improvements
Back to Blog

Performance Optimization in Next.js: Real Metrics, Real Improvements

March 21, 20263 min read88 views

A perfect Lighthouse score means nothing if your users are bouncing. Performance optimization that moves business metrics requires focusing on what actually matters: Largest Contentful Paint, Interaction to Next Paint, and Cumulative Layout Shift.

Core Web Vitals in 2026

Google's Core Web Vitals have evolved:

  • LCP (Largest Contentful Paint): Under 2.5s is good
  • INP (Interaction to Next Paint): Under 200ms is good
  • CLS (Cumulative Layout Shift): Under 0.1 is good
// Measure Core Web Vitals in your app
import { onLCP, onINP, onCLS } from 'web-vitals'

function sendToAnalytics(metric: Metric) {
  fetch('/api/analytics', {
    method: 'POST',
    body: JSON.stringify({
      name: metric.name,
      value: metric.value,
      id: metric.id,
      page: window.location.pathname
    })
  })
}

onLCP(sendToAnalytics)
onINP(sendToAnalytics)
onCLS(sendToAnalytics)

Image Optimization Beyond next/image

// Priority loading for LCP images
<Image
  src="/hero.jpg"
  alt="Hero"
  priority  // Preloads immediately
  sizes="(max-width: 768px) 100vw, 50vw"  // Correct sizing
  quality={85}  // Balance quality vs size
  placeholder="blur"  // Prevents CLS
  blurDataURL={heroBlurDataUrl}
/>

// Generate blur placeholders at build time
import { getPlaiceholder } from 'plaiceholder'

export async function getStaticProps() {
  const { base64 } = await getPlaiceholder('/hero.jpg')
  return { props: { blurDataURL: base64 } }
}

JavaScript Bundle Analysis

# Analyze your bundle
npx @next/bundle-analyzer

# Or with the built-in stats
next build --debug

Common wins:

  • Dynamic imports for heavy components
  • Replace moment.js with date-fns or dayjs
  • Use barrel file optimizations
  • Remove unused dependencies
// ❌ Imports everything
import { format } from 'date-fns'

// ✅ Tree-shakeable
import format from 'date-fns/format'

// Dynamic import for heavy components
const HeavyEditor = dynamic(() => import('@/components/Editor'), {
  loading: () => <EditorSkeleton />,
  ssr: false  // Skip SSR for client-only components
})

Streaming and Suspense

// Improve perceived performance with streaming
import { Suspense } from 'react'

export default async function ProductPage({ params }) {
  return (
    <>
      {/* Critical content renders immediately */}
      <ProductHeader productId={params.id} />
      
      {/* Non-critical content streams in */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews productId={params.id} />
      </Suspense>
      
      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations productId={params.id} />
      </Suspense>
    </>
  )
}

Real User Monitoring

// lib/monitoring.ts
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals'

export function initRUM() {
  const metrics: Record<string, number> = {}
  
  function report(metric) {
    metrics[metric.name] = metric.value
    
    // Report after page is stable
    if (Object.keys(metrics).length === 5) {
      sendToMonitoring({
        url: window.location.href,
        userAgent: navigator.userAgent,
        connection: navigator.connection?.effectiveType,
        ...metrics
      })
    }
  }
  
  onFCP(report)
  onTTFB(report)
  onLCP(report)
  onINP(report)
  onCLS(report)
}

Quick Wins Checklist

□ Priority hints on LCP images
□ Font subsetting and display:swap
□ Preconnect to critical origins
□ Dynamic imports for heavy components
□ Placeholder images to prevent CLS
□ Debounced event handlers for INP
□ Size hints on images and embeds
□ Route prefetching for navigation

Conclusion

Performance optimization is iterative. Set up Real User Monitoring, identify your worst pages, apply targeted fixes, and measure impact. Synthetic benchmarks are starting points—real user data drives decisions.

Share this article