DEV Community

Cover image for Core Web Vitals Optimization: Tackling LCP, CLS, and INP in Next.js 16
Sameer Hassan
Sameer Hassan

Posted on

Core Web Vitals Optimization: Tackling LCP, CLS, and INP in Next.js 16

Google's Core Web Vitals (CWV) are not just arbitrary vanity metrics. Since the integration of page experience signals into Google's core ranking systems and Answer Engine indexing, web applications with poor CWV suffer quantifiable organic traffic degradation and lower conversion rates.

Despite the modern capabilities of React 19 and Next.js 16 (Turbopack), many engineering teams struggle to pass the three primary metrics:

  1. Largest Contentful Paint (LCP ≤ 2.5s)
  2. Cumulative Layout Shift (CLS ≤ 0.1)
  3. Interaction to Next Paint (INP ≤ 200ms)

In ⚡ PLYXO (CRO • SEO • AIO • AEO • GEO), we built automated PageSpeed telemetry directly into our diagnostic pipeline. Here is our technical playbook for hitting sub-second LCP and zero CLS.


1. Diagnosing & Solving Largest Contentful Paint (LCP)

LCP measures the render time of the largest image or text block visible within the initial viewport. The most common pitfall is lazy-loading the hero image or delaying its discovery behind CSS background properties.

 Bad Pattern:
<div style={{ backgroundImage: "url('/hero.jpg')" }}>
<!-- Browser has to download HTML, parse CSS, compute styles, then start fetching -->

✅ Optimized Pattern:
<Image
  src="/hero.webp"
  alt="Dashboard Preview"
  priority={true}
  fetchPriority="high"
  loading="eager"
  sizes="(max-width: 768px) 100vw, 1200px"
  className="w-full h-auto"
/>
Enter fullscreen mode Exit fullscreen mode

Direct Link Preloading in Document Metadata

In Next.js 16, explicitly preload critical hero fonts and images in your server components:

export function HeroPreload() {
  return (
    <link
      rel="preload"
      as="image"
      href="/hero.webp"
      type="image/webp"
      fetchPriority="high"
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

2. Eliminating Cumulative Layout Shift (CLS)

CLS occurs when DOM elements shift their coordinates while surrounding assets (fonts, images, ad tags, dynamic widgets) are still downloading.

Pitfall A: Unsized Dynamic Containers

Never conditionally render client widgets without reserving their aspect ratio:

// ❌ Triggers 0.28 CLS when analytics chart hydrates:
export function ChartWrapper() {
  const { data, loading } = useAnalytics();
  if (loading) return null; // Shifts all content below downward!
  return <AnalyticsChart data={data} />;
}

// ✅ 0.00 CLS: Reserving exact geometry via Tailwind aspect-ratio:
export function ChartWrapper() {
  const { data, loading } = useAnalytics();
  return (
    <div className="w-full aspect-[16/9] min-h-[360px] rounded-xl bg-slate-900/40">
      {loading ? (
        <div className="w-full h-full animate-pulse rounded-xl bg-slate-800/50" />
      ) : (
        <AnalyticsChart data={data} />
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Pitfall B: Web Font Layout Flashes (FOUT)

Configure next/font with display: 'swap' and automated fallback metrics adjustments:

import { Inter } from 'next/font/google';

export const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  adjustFontFallback: true, // Prevents size jumps between fallback serif and Inter
});
Enter fullscreen mode Exit fullscreen mode

3. Taming Interaction to Next Paint (INP)

INP replaced First Input Delay (FID) to measure overall UI responsiveness throughout the entire session. If a user clicks a button and long-running JavaScript execution locks the main thread for > 200ms, the interaction fails.

Yielding to the Main Thread via scheduler.yield()

When executing heavy data filtering or DOM re-rendering, chunk work to let browser paint cycles execute:

async function processLargeDataset(items: DataItem[]) {
  const CHUNK_SIZE = 100;
  for (let i = 0; i < items.length; i += CHUNK_SIZE) {
    processBatch(items.slice(i, i + CHUNK_SIZE));

    // Yield execution to browser for user event processing & repaints
    if ('scheduler' in window && 'yield' in (window as any).scheduler) {
      await (window as any).scheduler.yield();
    } else {
      await new Promise(resolve => setTimeout(resolve, 0));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Automated Audits with Plyxo

Rather than running manual tests in Chrome DevTools, Plyxo continuously audits your live web properties, flags layout shift hot spots, and outputs exact line-by-line React remediation snippets.

👉 Try Plyxo's open-source performance scanner on GitHub

Top comments (0)