Back to Blog
Full-Stack

Next.js Performance Optimization: Crushing Core Web Vitals

Advanced techniques for optimizing Next.js apps: image optimization, code splitting, caching strategies, and achieving perfect Lighthouse scores.

13 min read
Updated Mar 1, 2026

Next.js Performance Optimization: Crushing Core Web Vitals

Google now ranks by performance metrics, not just content. Here's how to get green scores.

Understanding Core Web Vitals

Three metrics Google cares about:

1. LCP (Largest Contentful Paint): Largest visible element renders. Target: <2.5s

2. FID (First Input Delay): Time from user input to JS response. Target: <100ms

3. CLS (Cumulative Layout Shift): How much does layout move? Target: <0.1

Poor scores = lower rankings = less traffic = missed revenue.

Image Optimization: The Biggest Win

Use Next.js Image Component

Instead of:

<img src="/hero.jpg" alt="Hero" /> // BAD: no optimization

Hero // BAD: no optimization


Do this:

Do this:

import Image from "next/image";

<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={630}
  priority // Only for LCP images
  sizes="(max-width: 640px) 100vw, 50vw"
/>

import Image from "next/image";

src="/hero.jpg"

alt="Hero"

width={1200}

height={630}

priority // Only for LCP images

sizes="(max-width: 640px) 100vw, 50vw"

/>


Next.js automatically:
- Resizes for different screen sizes
- Converts to WebP (30% smaller)
- Lazy loads below-the-fold
- Prevents layout shift (fixed aspect ratio)

### Image Formats Matter

WebP is 25-35% smaller than JPEG. AVIF is another 20% smaller.

Let the browser choose:

Next.js automatically:

  • Resizes for different screen sizes
  • Converts to WebP (30% smaller)
  • Lazy loads below-the-fold
  • Prevents layout shift (fixed aspect ratio)
  • Image Formats Matter

    WebP is 25-35% smaller than JPEG. AVIF is another 20% smaller.

    Let the browser choose:

    <picture>
      <source srcSet="/image.avif" type="image/avif" />
      <source srcSet="/image.webp" type="image/webp" />
      <img src="/image.jpg" alt="Fallback" />
    </picture>

    Fallback

    
    Or let Next.js handle it with `priority` and `sizes` attributes.
    
    ## Code Splitting & Lazy Loading
    
    ### Dynamic Imports
    
    Don't load code users won't see:
    

    Or let Next.js handle it with `priority` and `sizes` attributes.

    Code Splitting & Lazy Loading

    Dynamic Imports

    Don't load code users won't see:

    import dynamic from "next/dynamic";
    
    const HeavyDashboard = dynamic(() => import("./dashboard"), {
      loading: <Skeleton />
    });
    
    export default function Home() {
      return <HeavyDashboard />; // Only loads when Home renders
    }

    import dynamic from "next/dynamic";

    const HeavyDashboard = dynamic(() => import("./dashboard"), {

    loading:

    });

    export default function Home() {

    return ; // Only loads when Home renders

    }

    
    ### Route-Based Code Splitting
    
    With Next.js App Router, every page is an automatic code split. A user on /contact doesn't download /blog code.
    
    ### Component-Level Splitting
    
    Split large components:
    

    Route-Based Code Splitting

    With Next.js App Router, every page is an automatic code split. A user on /contact doesn't download /blog code.

    Component-Level Splitting

    Split large components:

    const Modal = dynamic(() => import("./modal"), {
      ssr: false, // Don't render on server
    });

    const Modal = dynamic(() => import("./modal"), {

    ssr: false, // Don't render on server

    });

    
    ## Fonts: Often Overlooked
    
    System fonts load instantly. Google Fonts? Not so much.
    

    Fonts: Often Overlooked

    System fonts load instantly. Google Fonts? Not so much.

    // next.config.ts
    const nextConfig = {
      optimizeFonts: true,
    };

    // next.config.ts

    const nextConfig = {

    optimizeFonts: true,

    };

    
    Better: use `display: swap` to show fallback while loading:
    

    Better: use `display: swap` to show fallback while loading:

    @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap");

    @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap");

    
    Or self-host fonts (eliminates Google network request):
    

    Or self-host fonts (eliminates Google network request):

    npm install @next/font

    npm install @next/font

    import { Inter } from "@next/font/google";
    
    const inter = Inter({ subsets: ["latin"] });
    
    export default function RootLayout() {
      return (
        <html className={inter.className}>
          ...
        </html>
      );
    }

    import { Inter } from "@next/font/google";

    const inter = Inter({ subsets: ["latin"] });

    export default function RootLayout() {

    return (

    ...

    );

    }

    
    ## Caching Strategy
    
    ### Static Generation (Fastest)
    
    Build-time HTML. Perfect for marketing pages:
    

    Caching Strategy

    Static Generation (Fastest)

    Build-time HTML. Perfect for marketing pages:

    export const revalidate = 86400; // Revalidate once per day
    
    export default function About() {
      return <div>About Page</div>;
    }

    export const revalidate = 86400; // Revalidate once per day

    export default function About() {

    return

    About Page
    ;

    }

    
    ### ISR (Incremental Static Regeneration)
    
    Revalidate in background:
    

    ISR (Incremental Static Regeneration)

    Revalidate in background:

    export const revalidate = 3600; // Revalidate every hour
    
    export default function Blog({ params }: { params: { slug: string } }) {
      const post = getPost(params.slug);
      return <article>{post.content}</article>;
    }

    export const revalidate = 3600; // Revalidate every hour

    export default function Blog({ params }: { params: { slug: string } }) {

    const post = getPost(params.slug);

    return

    {post.content}
    ;

    }

    
    Google crawls static pages. They rank better.
    
    ### Server Components (When Content Changes)
    
    Use server components for fresh data:
    

    Google crawls static pages. They rank better.

    Server Components (When Content Changes)

    Use server components for fresh data:

    // No "use client" = server component
    
    export default async function Dashboard() {
      const data = await fetch("https://api.example.com/data");
      return <div>{data}</div>;
    }

    // No "use client" = server component

    export default async function Dashboard() {

    const data = await fetch("https://api.example.com/data");

    return

    {data}
    ;

    }

    
    ## JavaScript Bloat
    
    ### Third-Party Scripts
    
    Scripts like analytics and ads murder performance. Load them asynchronously:
    

    JavaScript Bloat

    Third-Party Scripts

    Scripts like analytics and ads murder performance. Load them asynchronously:

    <Script
      src="https://cdn.example.com/analytics.js"
      strategy="lazyOnload"
    />

    src="https://cdn.example.com/analytics.js"

    strategy="lazyOnload"

    />

    
    Strategies:
    - `beforeInteractive`: Critical (auth, theme)
    - `afterInteractive`: Normal (analytics, ads)
    - `lazyOnload`: Low priority (chat widget)
    
    ### React DevTools in Production?
    
    That's costing you 500ms+ LCP. Use:
    

    Strategies:

  • `beforeInteractive`: Critical (auth, theme)
  • `afterInteractive`: Normal (analytics, ads)
  • `lazyOnload`: Low priority (chat widget)
  • React DevTools in Production?

    That's costing you 500ms+ LCP. Use:

    if (process.env.NODE_ENV === "production") {
      // Remove debugging libraries
    }

    if (process.env.NODE_ENV === "production") {

    // Remove debugging libraries

    }

    
    ## Web Vitals Monitoring
    
    Install the library:
    

    Web Vitals Monitoring

    Install the library:

    npm install web-vitals

    npm install web-vitals

    
    Track metrics:
    

    Track metrics:

    import { getCLS, getFID, getFCP, getLCP, getTTFB } from "web-vitals";
    
    getCLS(console.log);
    getFID(console.log);
    getLCP(console.log);

    import { getCLS, getFID, getFCP, getLCP, getTTFB } from "web-vitals";

    getCLS(console.log);

    getFID(console.log);

    getLCP(console.log);

    
    Send to analytics:
    

    Send to analytics:

    function sendToAnalytics(metric) {
      fetch("/api/analytics", {
        method: "POST",
        body: JSON.stringify(metric),
      });
    }
    
    getLCP(sendToAnalytics);

    function sendToAnalytics(metric) {

    fetch("/api/analytics", {

    method: "POST",

    body: JSON.stringify(metric),

    });

    }

    getLCP(sendToAnalytics);

    
    Now you can see which pages are slow in production.
    
    ## Layout Shift Fixes
    
    ### Specify Image Dimensions
    
    Missing heights cause CLS:
    

    Now you can see which pages are slow in production.

    Layout Shift Fixes

    Specify Image Dimensions

    Missing heights cause CLS:

    // Good
    <Image width={200} height={200} src="..." />
    
    // Bad
    <Image src="..." /> // No height = layout shift waiting for load

    // Good

    // Bad

    // No height = layout shift waiting for load

    
    ### Reserve Space for Ads/Embeds
    

    Reserve Space for Ads/Embeds

    .ad-container {
      width: 300px;
      height: 250px; /* Reserved space */
      overflow: hidden;
    }

    .ad-container {

    width: 300px;

    height: 250px; / Reserved space /

    overflow: hidden;

    }

    
    ## The Checklist
    
    - [ ] All LCP images use `priority={true}`
    - [ ] Web fonts use `display: swap`
    - [ ] LCP < 2.5s
    - [ ] FID/INP < 100ms (avoid large JS)
    - [ ] CLS < 0.1 (all heights specified)
    - [ ] Minified CSS/JS
    - [ ] Images use WebP/AVIF
    - [ ] Third-party scripts use `lazyOnload`
    - [ ] No unused CSS/JS shipped
    
    ## Result
    
    With these optimizations, most Next.js sites score 90+ on Lighthouse. And Google ranks them higher.
    
    Performance is a feature. Treat it like one.
        

    The Checklist

  • [ ] All LCP images use `priority={true}`
  • [ ] Web fonts use `display: swap`
  • [ ] LCP < 2.5s
  • [ ] FID/INP < 100ms (avoid large JS)
  • [ ] CLS < 0.1 (all heights specified)
  • [ ] Minified CSS/JS
  • [ ] Images use WebP/AVIF
  • [ ] Third-party scripts use `lazyOnload`
  • [ ] No unused CSS/JS shipped
  • Result

    With these optimizations, most Next.js sites score 90+ on Lighthouse. And Google ranks them higher.

    Performance is a feature. Treat it like one.

    #Next.js#Performance#Optimization#Core Web Vitals#SEO
    Vasanth Kumar

    Full-Stack Engineer & AI Product Builder

    4+ years of experience building scalable web applications and AI-powered products. Passionate about end-to-end product development, clean architecture, and solving real-world problems.

    More Articles