The most surprising true thing about Next.js Core Web Vitals is that you can often achieve significant improvements without touching a single line of framework-specific code.

Let’s see what that looks like in practice. Imagine a typical Next.js e-commerce product page. When a user lands, their browser needs to download HTML, CSS, JavaScript, and images. The browser then parses these, executes JavaScript to render the page, and potentially fetches more data. Core Web Vitals measure different aspects of this user experience:

  • Largest Contentful Paint (LCP): When the largest content element (usually an image or text block) becomes visible.
  • Interaction to Next Paint (INP): The latency of all interactions a user has with the page. This is the successor to FID (First Input Delay).
  • Cumulative Layout Shift (CLS): Measures unexpected layout shifts.

Here’s a simplified view of a product page’s critical rendering path in Next.js:

<!DOCTYPE html>
<html>
<head>
  <title>Awesome T-Shirt</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <link rel="preload" href="/_next/static/css/styles.css" as="style" />
  <link rel="stylesheet" href="/_next/static/css/styles.css" />
  <script defer="" src="/_next/static/chunks/webpack-123456.js"></script>
  <script defer="" src="/_next/static/chunks/framework-abcdef.js"></script>
  <script defer="" src="/_next/static/chunks/main-ghijkl.js"></script>
  <script defer="" src="/_next/static/chunks/pages/_app-mnop.js"></script>
  <script defer="" src="/_next/static/chunks/pages/product/[id]-qrst.js"></script>
  <meta name="next-head-count" content="5" />
</head>
<body>
  <div id="__next">
    <main>
      <h1>Awesome T-Shirt</h1>
      <img src="/images/tshirt.jpg" alt="Awesome T-Shirt" width="600" height="400" />
      <p>This is the best t-shirt ever.</p>
      <button>Add to Cart</button>
    </main>
  </div>
</body>
</html>

The core problem Next.js solves is delivering these assets efficiently, especially with its hybrid rendering (SSR, SSG, ISR) and client-side navigation. However, performance bottlenecks often arise from the content itself and how it’s loaded, rather than just the framework.

LCP Focus: The Hero Image

Your LCP element is likely the <img> tag. To improve LCP, we need to make sure that image gets to the user as quickly and as early as possible.

  1. Image Optimization (Next/Image):

    • Diagnosis: Use browser DevTools (Performance tab) to record a page load. Look for the LCP element and check its load time. If it’s an unoptimized image, you’ll see a large file size.
    • Fix: Replace your <img> tag with Next.js’s built-in next/image component.
      import Image from 'next/image';
      
      function ProductPage({ product }) {
        return (
          <main>
            <h1>{product.name}</h1>
            <Image
              src={product.imageUrl}
              alt={product.name}
              width={600}
              height={400}
              priority // Crucial for LCP elements!
            />
            <p>{product.description}</p>
            <button>Add to Cart</button>
          </main>
        );
      }
      
    • Why it works: next/image automatically optimizes images (resizing, modern formats like WebP, lazy loading for non-critical images). Adding priority tells Next.js to inline the image’s src directly into the HTML and to fetch it with a high priority, often bypassing lazy loading and ensuring it’s available for the browser’s LCP calculation.
  2. Resource Hints:

    • Diagnosis: In the Network tab of DevTools, observe the order of resource loading. If your LCP image is a cross-origin resource (e.g., from a CDN), it might not be prioritized correctly.
    • Fix: Use next/head to add dns-prefetch and preconnect hints for your image CDN.
      import Head from 'next/head';
      
      function ProductPage({ product }) {
        return (
          <>
            <Head>
              <title>{product.name}</title>
              <link rel="preconnect" href="https://your-image-cdn.com" />
              <link rel="dns-prefetch" href="https://your-image-cdn.com" />
            </Head>
            <main>
              {/* ... rest of your page */}
            </main>
          </>
        );
      }
      
    • Why it works: dns-prefetch resolves the DNS for a domain, and preconnect goes further by establishing a TCP connection and TLS handshake. This significantly reduces the time it takes for the browser to start downloading the image once it’s discovered.

INP Focus: Interactivity and JavaScript

INP measures how quickly your page responds to user interactions. This often comes down to JavaScript execution blocking the main thread.

  1. Code Splitting and Dynamic Imports:

    • Diagnosis: In the Performance tab, look for long tasks (red triangles) during initial load or after user interaction. If these are related to JavaScript execution, especially for components not immediately visible, you have a problem.
    • Fix: Use next/dynamic for components that aren’t critical for the initial render.
      import dynamic from 'next/dynamic';
      
      const DynamicReviewSection = dynamic(() => import('../components/ReviewSection'), {
        loading: () => <p>Loading reviews...</p>,
        ssr: false // Only load on the client
      });
      
      function ProductPage({ product }) {
        return (
          <main>
            {/* ... */}
            <DynamicReviewSection productId={product.id} />
          </main>
        );
      }
      
    • Why it works: next/dynamic creates separate JavaScript bundles for the imported component. These bundles are only downloaded and executed when the component is actually needed (e.g., scrolled into view or interacted with), reducing the initial JavaScript payload and freeing up the main thread.
  2. Efficient Data Fetching:

    • Diagnosis: Observe the waterfall in the Network tab. If your page is waiting for multiple API calls sequentially after the initial HTML load (especially in getServerSideProps or client-side fetch), it can delay rendering and subsequent interactivity.
    • Fix: Use React.use with Suspense for concurrent data fetching (available in newer React/Next.js versions).
      // In your page component (e.g., using App Router)
      import { Suspense } from 'react';
      import ProductDetails from './ProductDetails';
      import Reviews from './Reviews';
      import { fetchProduct, fetchReviews } from './api'; // Assume these return Promises
      
      async function ProductPage({ productId }) {
        const product = await fetchProduct(productId);
        return (
          <main>
            <h1>{product.name}</h1>
            <Image src={product.imageUrl} ... priority />
            <Suspense fallback={<p>Loading reviews...</p>}>
              <Reviews productId={productId} />
            </Suspense>
          </main>
        );
      }
      
      // In ./Reviews component
      async function Reviews({ productId }) {
        const reviews = await fetchReviews(productId);
        return (
          <div>
            <h2>Reviews</h2>
            {reviews.map(r => <p key={r.id}>{r.text}</p>)}
          </div>
        );
      }
      
    • Why it works: Suspense allows React to "wait" for asynchronous data to resolve without blocking the entire render. When combined with concurrent features, React can start rendering other parts of the component tree while waiting for data, and then seamlessly stitch in the resolved data. This leads to faster perceived load times and better INP as the main thread is less occupied with sequential data fetches.

CLS Focus: Layout Instability

CLS is often caused by elements that load late or change size dynamically.

  1. Specifying Dimensions for Images and Embeds:

    • Diagnosis: In DevTools, the "Experience" tab (or "Rendering" tab for layout shift regions) highlights areas that shift. You’ll often see content jump down as an image or ad loads without reserved space.
    • Fix: Always provide width and height attributes for <img> tags (or use next/image which requires them). For dynamically sized content like iframes or ads, reserve space using CSS.
      // Already covered with next/image above
      <Image
        src={product.imageUrl}
        alt={product.name}
        width={600}
        height={400}
        priority
      />
      
      // For ads or embeds
      
      <div style={{ width: '100%', height: '250px', position: 'relative' }}>
      
        <iframe
          src="ad-url"
      
          style={{ width: '100%', height: '100%', border: 'none' }}
      
          loading="lazy" // Use lazy loading if possible
        ></iframe>
      </div>
      
    • Why it works: By specifying dimensions, the browser can allocate the necessary space for the element before it loads. This prevents content from reflowing and shifting the layout once the element finally appears.
  2. Avoiding Inserted DOM Elements Without Reserved Space:

    • Diagnosis: Similar to the above, look for content that pushes existing content down unexpectedly. This is common with injected elements like cookie banners or dynamic UI updates.
    • Fix: If you must insert content dynamically, ensure the container has a defined height or that the content is appended in a way that doesn’t cause reflows (e.g., using absolute positioning if appropriate, or ensuring the parent container has a minimum height).
      /* Example for a cookie banner */
      .cookie-banner {
        position: fixed; /* or absolute */
        bottom: 0;
        left: 0;
        width: 100%;
        background-color: white;
        padding: 1rem;
        box-sizing: border-box;
        z-index: 1000;
        /* No height specified here if it can grow, but it won't push content */
      }
      
    • Why it works: Elements that are fixed or absolutely positioned are taken out of the normal document flow, so they don’t affect the layout of other elements when they appear or disappear.

The most powerful lever for Core Web Vitals in Next.js often lies in understanding how the browser renders your content, not just how Next.js bundles it. This means focusing on image optimization, efficient JavaScript loading, and reserving space for elements that might appear late.

Once you’ve optimized your LCP, INP, and CLS, you’ll likely start looking at how to make your initial server response even faster, which leads you into the realm of caching strategies and edge functions.

Want structured learning?

Take the full Nextjs course →