SUBID DASPORTFOLIO / 2026
000

SUBID

PORTRAIT // ORIGINAL[ 1 / 7 ]
WORK // RIDENOWW[ 2 / 7 ]
WORK // DEED POLL[ 3 / 7 ]
PORTRAIT // CYBERNETIC[ 4 / 7 ]
PORTRAIT // HOLOGRAM[ 5 / 7 ]
PORTRAIT // SKETCH[ 6 / 7 ]
PORTRAIT // GLITCH[ 7 / 7 ]
SUBID DAS
Back to Blog

Next.js Performance Optimization: From First Contentful Paint to Core Web Vitals

10 min readSubid Das
nextjsperformanceweb-vitalsoptimizationfrontend

Performance is a feature. Users expect fast websites, and search engines reward fast sites with better rankings. In fact, studies show that a 1-second delay in page load can result in a 7% reduction in conversions.

Next.js provides excellent built-in optimizations, but many developers don't leverage them fully. In this comprehensive guide, I'll show you how to squeeze every millisecond of performance from your Next.js application.

Understanding Core Web Vitals

Google's Core Web Vitals are three key metrics that measure user experience:

Largest Contentful Paint (LCP)

The time it takes for the largest content element (text, image, video) to become visible.

Target: < 2.5 seconds

// ❌ Bad: Large images without optimization
export default function Hero() {
  return <img src="/hero.jpg" width={1920} height={1080} />
}

// ✅ Good: Using Next.js Image component
import Image from "next/image"

export default function Hero() {
  return (
    <Image
      src="/hero.jpg"
      width={1920}
      height={1080}
      priority  // Loads in high priority for LCP
      placeholder="blur"
      blurDataURL="data:image/..."
    />
  )
}

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

The time before the browser can respond to user input. INP is the newer replacement for FID.

Target: < 100ms

// ❌ Bad: Heavy computation blocks main thread
export default function Filter({ items }) {
  const [search, setSearch] = useState("")

  // This runs on every keystroke!
  const filtered = items.filter(item => 
    JSON.stringify(item).includes(search)
  )

  return (
    <>
      <input onChange={(e) => setSearch(e.target.value)} />
      {filtered.map(item => <ItemCard key={item.id} item={item} />)}
    </>
  )
}

// ✅ Good: Debounced search with Web Workers
import { useDeferredValue } from "react"

export default function Filter({ items }) {
  const [search, setSearch] = useState("")
  const deferredSearch = useDeferredValue(search)

  const filtered = items.filter(item => 
    item.name.toLowerCase().includes(deferredSearch.toLowerCase())
  )

  return (
    <>
      <input onChange={(e) => setSearch(e.target.value)} />
      <Suspense fallback={<Skeleton />}>
        {filtered.map(item => <ItemCard key={item.id} item={item} />)}
      </Suspense>
    </>
  )
}

Cumulative Layout Shift (CLS)

Unexpected layout shifts that occur during page load.

Target: < 0.1

// ❌ Bad: Image without dimensions causes layout shift
<div className="grid grid-cols-3">
  <img src="/product.jpg" />  {/* No width/height */}
</div>

// ✅ Good: Always specify dimensions
<div className="grid grid-cols-3">
  <Image
    src="/product.jpg"
    width={300}
    height={300}
    placeholder="blur"
  />
</div>

// ✅ Also good: Use aspect ratio container
<div className="relative w-full aspect-square">
  <Image src="/product.jpg" fill alt="Product" />
</div>

Image Optimization Mastery

Images are typically the largest assets. Next.js Image component handles this automatically.

Responsive Images

import Image from "next/image"

export default function ProductImage() {
  return (
    <Image
      src="/product.jpg"
      width={800}
      height={600}
      sizes="(max-width: 640px) 100vw,
             (max-width: 1024px) 50vw,
             33vw"
      quality={90}
    />
  )
}

The sizes prop is crucial—it tells Next.js which image sizes to generate:

  • Mobile: 100% of viewport
  • Tablet: 50% of viewport
  • Desktop: 33% of viewport

Modern Formats

Next.js automatically generates WebP and AVIF formats:

// Next.js automatically:
// 1. Generates WebP (saves ~25% vs JPEG)
// 2. Generates AVIF (saves ~40% vs JPEG)
// 3. Selects best format based on browser support
// 4. Lazy loads below-the-fold images
// 5. Uses placeholder while loading

<Image src="/image.jpg" alt="Example" width={400} height={300} />

// Results in:
// <img srcset="
//   /image-400w.avif 1x,
//   /image-800w.avif 2x,
//   /image-400w.webp 1x,
//   /image-800w.webp 2x,
//   /image-400w.jpg 1x,
//   /image-800w.jpg 2x
// " />

Code Splitting and Lazy Loading

Dynamic Imports

// pages/dashboard.tsx
import dynamic from "next/dynamic"
import { Suspense } from "react"

const HeavyAnalytics = dynamic(
  () => import("@/components/Analytics"),
  { 
    loading: () => <div>Loading charts...</div>,
    ssr: false  // Don't render on server
  }
)

export default function Dashboard() {
  return (
    <>
      <Header />
      <Suspense fallback={<Skeleton />}>
        <HeavyAnalytics />
      </Suspense>
    </>
  )
}

This splits the Analytics component into a separate chunk, loaded only when needed.

Route-based Code Splitting

Next.js automatically splits code per route. A user visiting /dashboard doesn't download code for /settings.

Initial page load: 45 KB
Route change to /dashboard: +12 KB
Route change to /settings: +8 KB

Versus bundling everything: 100 KB upfront.

Font Optimization

Google Fonts cause a major performance impact if not optimized.

// pages/_document.tsx
import { Html, Head, Main, NextScript } from "next/document"

export default function Document() {
  return (
    <Html lang="en">
      <Head>
        {/* ❌ Bad: External font URL blocks rendering */}
        {/* <link href="https://fonts.googleapis.com/css?family=Poppins" /> */}

        {/* ✅ Good: self-hosted with font preloading */}
        <link
          rel="preload"
          href="/fonts/poppins-variable.woff2"
          as="font"
          type="font/woff2"
          crossOrigin="anonymous"
        />
        <style>
          {`
            @font-face {
              font-family: 'Poppins';
              src: url('/fonts/poppins-variable.woff2') format('woff2-variations');
              font-weight: 100 900;
            }
          `}
        </style>
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}

Self-hosting fonts eliminates DNS lookups and can save 100-300ms.

Build-time Optimization

Static Generation (SSG)

// pages/blog/[slug].tsx
export async function getStaticProps({ params }) {
  const post = await getPost(params.slug)
  
  return {
    props: { post },
    revalidate: 3600  // Regenerate every hour
  }
}

export async function getStaticPaths() {
  const posts = await getAllPosts()
  
  return {
    paths: posts.map(post => ({
      params: { slug: post.slug }
    })),
    fallback: "blocking"
  }
}

export default function BlogPost({ post }) {
  return <article>{post.content}</article>
}

SSG pages are pre-rendered at build time → instant delivery. Use ISR (Incremental Static Regeneration) to keep content fresh.

Incremental Static Regeneration (ISR)

export async function getStaticProps() {
  return {
    props: { data },
    revalidate: 60  // Regenerate after 60 seconds
  }
}

First request after 60 seconds triggers a background rebuild. Users get fresh content without waiting.

Monitoring Performance

Built-in Analytics

// pages/_app.tsx
import { useReportWebVitals } from "next/web-vitals"

export default function App({ Component, pageProps }) {
  useReportWebVitals(metric => {
    console.log(metric)
    
    // Send to analytics
    fetch("/api/metrics", {
      method: "POST",
      body: JSON.stringify(metric)
    })
  })

  return <Component {...pageProps} />
}

Performance API

export function measurePerformance() {
  const metrics = performance.getEntriesByType("navigation")[0]
  
  console.log({
    DNS: metrics.domainLookupEnd - metrics.domainLookupStart,
    TCP: metrics.connectEnd - metrics.connectStart,
    TTFB: metrics.responseStart - metrics.requestStart,
    Download: metrics.responseEnd - metrics.responseStart,
    DOMParse: metrics.domInteractive - metrics.domLoading,
    Resources: metrics.loadEventStart - metrics.domInteractive,
    Total: metrics.loadEventEnd - metrics.fetchStart
  })
}

Real-World Results

I applied these optimizations to a client's e-commerce site:

MetricBeforeAfterImprovement
LCP3.2s1.8s44% faster
FID145ms52ms64% faster
CLS0.150.0567% better
Total Bundle250KB89KB64% smaller

Google Lighthouse score improved from 62 to 94.

Performance Checklist

  • Use Next.js Image component for all images
  • Set image sizes prop for responsive behavior
  • Add priority prop to LCP images
  • Implement dynamic imports for heavy components
  • Self-host Google Fonts
  • Use next/script for third-party scripts
  • Implement ISR for content-heavy pages
  • Monitor Core Web Vitals with analytics
  • Test on real 4G networks (DevTools)
  • Use Lighthouse CI in CI/CD pipeline

Conclusion

Next.js provides all the tools needed to build blazingly fast applications. The key is understanding why each optimization matters and measuring the impact.

Start with the biggest wins: image optimization and code splitting. Then progressively apply other techniques as needed.

Performance is not a one-time task—it's an ongoing commitment. Monitor, measure, and iterate continuously.

What optimization technique has helped your Next.js app the most? Share in the comments below!

About the author

Subid Das is a cloud native engineer and open source contributor. Find more articles onthe blog.

Open to freelance, full-time, and interesting problems.

LET'S CONNECT