Back to blog
Web Design and Development

Core Web Vitals 2026: Optimization for Next.js & React

Featured image: core web vitals 2026 nextjs react

Core Web Vitals 2026: Optimization for Next.js and React

Core Web Vitals have evolved significantly since their introduction in 2020. In 2026, with the establishment of INP (Interaction to Next Paint) as an official metric and the advanced capabilities of Next.js 15, optimizing web performance requires a completely renewed approach. This technical guide shows you exactly how to master each metric using the most modern tools in the React ecosystem.

Google has confirmed that sites exceeding Core Web Vitals thresholds experience 24% less abandonment and better search result rankings. For projects developed with React and Next.js, the optimization opportunities are enormous, but so are the technical challenges.

What Are Core Web Vitals in 2026 and Why Do They Matter?

Core Web Vitals are a set of user-centric metrics that measure the real experience of loading, interactivity, and visual stability of a web page. In 2026, the three fundamental metrics are:

  • LCP (Largest Contentful Paint): Time until the largest element in the viewport is visible
  • INP (Interaction to Next Paint): Latency of all user interactions
  • CLS (Cumulative Layout Shift): Visual stability throughout the session

Unlike synthetic metrics like Time to First Byte or Speed Index, Core Web Vitals reflect the perceived experience of real users browsing under real network and device conditions.

Updated Thresholds for 2026

The most significant change was the replacement of FID (First Input Delay) by INP in March 2024. While FID only measured the first interaction, INP evaluates the latency of all interactions during the session, offering a much more realistic view of interactivity.

How Does LCP Work and How to Optimize It in Next.js 15?

LCP measures the time it takes to render the largest visible content element in the viewport. Typically, this element is a hero image, a prominent text block, or a video poster.

Elements That Qualify for LCP

  • elements (including within )
  • Background images loaded via url() in CSS
  • elements with poster
  • Text elements (

    ,

    , etc.) with significant content

  • Elements with inline background images

LCP Optimization Techniques in Next.js 15

1. Correct Use of the Image Component

Next.js 15 includes an optimized Image component that implements smart lazy loading, but for the LCP element, we need the opposite behavior:

// app/page.tsx
import Image from 'next/image';

export default function HomePage() {
  return (
    <main>
      <section className="hero">
        {/* LCP Image: priority + fetchPriority */}
        <Image
          src="/hero-banner.webp"
          alt="Hero description"
          width={1920}
          height={1080}
          priority // Disables lazy loading
          fetchPriority="high" // Hint to the browser
          sizes="100vw"
          quality={85}
        />
      </section>
      
      {/* Other images: default lazy loading */}
      <Image
        src="/secondary-image.webp"
        alt="Secondary image"
        width={800}
        height={600}
        loading="lazy"
      />
    </main>
  );
}

2. Preload Critical Resources

In the Next.js 15 App Router, you can manage preloads directly in the layout:

// app/layout.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'My Website',
  other: {
    'link': [
      {
        rel: 'preload',
        href: '/fonts/inter-var.woff2',
        as: 'font',
        type: 'font/woff2',
        crossOrigin: 'anonymous',
      },
    ],
  },
};

// Or using the head
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <link
          rel="preload"
          href="/hero-banner.webp"
          as="image"
          fetchPriority="high"
        />
        <link
          rel="preconnect"
          href="https://fonts.googleapis.com"
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

3. Streaming and Suspense for LCP

An advanced technique is using Suspense to prioritize LCP content:

// app/page.tsx
import { Suspense } from 'react';
import HeroBanner from '@/components/HeroBanner';
import DynamicContent from '@/components/DynamicContent';

export default function Page() {
  return (
    <>
      {/* LCP Content renders immediately */}
      <HeroBanner />
      
      {/* Secondary content in streaming */}
      <Suspense fallback={<ContentSkeleton />}>
        <DynamicContent />
      </Suspense>
    </>
  );
}

What Is INP and Why Did It Replace FID?

INP (Interaction to Next Paint) measures the latency between a user interaction and the next updated visual frame. Unlike FID, which only considered the first interaction, INP evaluates all interactions and reports a value close to the worst case.

Anatomy of an Interaction

Each interaction has three phases:

  1. Input delay: Time from the event until the handler begins
  2. Processing time: Time executing the event handlers
  3. Presentation delay: Time until the browser renders the frame
[Click] → [Input Delay] → [Processing] → [Presentation] → [Visual Update]
         └─────────────── INP Total ───────────────────┘

Interactions Counted by INP

  • Mouse clicks
  • Taps on touch screens
  • Key presses (both physical and on-screen keyboard)

Not counted: Hover, scroll, or zoom (considered continuous interactions).

INP Optimization in React and Next.js

1. Avoid Main Thread Blocking

The most common INP issue in React is extensive synchronous renders:

// ❌ BAD: Costly re-render blocks interactions
function ProductList({ products }) {
  const [filter, setFilter] = useState('');
  
  // Synchronous filtering on each render
  const filtered = products.filter(p => 
    p.name.toLowerCase().includes(filter.toLowerCase())
  );
  
  return (
    <>
      <input 
        value={filter} 
        onChange={e => setFilter(e.target.value)} 
      />
      {filtered.map(p => <ProductCard key={p.id} product={p} />)}
    </>
  );
}

// ✅ GOOD: useDeferredValue to prioritize input
import { useDeferredValue, useMemo } from 'react';

function ProductList({ products }) {
  const [filter, setFilter] = useState('');
  const deferredFilter = useDeferredValue(filter);
  
  const filtered = useMemo(() => 
    products.filter(p => 
      p.name.toLowerCase().includes(deferredFilter.toLowerCase())
    ),
    [products, deferredFilter]
  );
  
  const isStale = filter !== deferredFilter;
  
  return (
    <>
      <input 
        value={filter} 
        onChange={e => setFilter(e.target.value)} 
      />
      <div style={{ opacity: isStale ? 0.7 : 1 }}>
        {filtered.map(p => <ProductCard key={p.id} product={p} />)}
      </div>
    </>
  );
}

2. useTransition for Non-Urgent Updates

import { useState, useTransition } from 'react';

function TabContainer() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();
  
  function handleTabChange(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }
  
  return (
    <div>
      <nav>
        {['home', 'products', 'about'].map(t => (
          <button
            key={t}
            onClick={() => handleTabChange(t)}
            className={tab === t ? 'active' : ''}
          >
            {t}
          </button>
        ))}
      </nav>
      
      <div style={{ opacity: isPending ? 0.8 : 1 }}>
        <TabContent tab={tab} />
      </div>
    </div>
  );
}

3. Virtualization for Long Lists

import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualizedList({ items }) {
  const parentRef = useRef(null);
  
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 80,
    overscan: 5,
  });
  
  return (
    <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map(virtualItem => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: 0,
              transform: `translateY(${virtualItem.start}px)`,
              height: `${virtualItem.size}px`,
            }}
          >
            <ItemCard item={items[virtualItem.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

How to Prevent CLS in Modern React Applications?

CLS (Cumulative Layout Shift) measures unexpected layout shifts during the entire lifespan of the page. High CLS frustrates users trying to interact with moving elements.

Common Causes of CLS

  1. Images without dimensions: The browser doesn't reserve space until the image is downloaded
  2. Dynamic ads and embeds: Injected content that shifts the layout
  3. Web fonts (FOIT/FOUT): Size changes when loading fonts
  4. Dynamically inserted content: Banners, notifications, modals

Specific Solutions for Next.js 15

1. Aspect Ratio for Media

// Image component with guaranteed aspect ratio
function ResponsiveImage({ src, alt, aspectRatio = '16/9' }) {
  return (
    <div style={{ aspectRatio, position: 'relative' }}>
      <Image
        src={src}
        alt={alt}
        fill
        style={{ objectFit: 'cover' }}
        sizes="(max-width: 768px) 100vw, 50vw"
      />
    </div>
  );
}

2. Skeleton Loaders with Fixed Dimensions

// components/ProductSkeleton.tsx
export function ProductSkeleton() {
  return (
    <div className="product-card" style={{ height: '320px' }}>
      <div 
        className="skeleton" 
        style={{ height: '200px', background: '#e0e0e0' }} 
      />
      <div 
        className="skeleton" 
        style={{ height: '24px', marginTop: '16px', width: '80%' }} 
      />
      <div 
        className="skeleton" 
        style={{ height: '20px', marginTop: '8px', width: '40%' }} 
      />
    </div>
  );
}

// Use with Suspense
import { Suspense } from 'react';

export default function ProductPage() {
  return (
    <Suspense fallback={<ProductSkeleton />}>
      <ProductDetails />
    </Suspense>
  );
}

3. Font Optimization in Next.js 15

// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // Avoids FOIT
  variable: '--font-inter',
  preload: true,
  fallback: ['system-ui', 'sans-serif'],
  adjustFontFallback: true, // Adjusts fallback metrics
});

const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-mono',
});

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
      <body className={inter.className}>
        {children}
      </body>
    </html>
  );
}

4. Reserve Space for Dynamic Content

// Banner component with reserved height
function PromoBanner() {
  const [isVisible, setIsVisible] = useState(true);
  
  if (!isVisible) {
    // Maintain space or collapse smoothly
    return null;
  }
  
  return (
    <div 
      className="promo-banner"
      style={{ 
        minHeight: '60px', // Reserved height
        containIntrinsicSize: '0 60px', // content-visibility optimization
      }}
    >
      <p>Special Offer! 20% Off</p>
      <button onClick={() => setIsVisible(false)}>×</button>
    </div>
  );
}

How Do React Server Components Impact Core Web Vitals?

React Server Components (RSC), fully integrated into Next.js 15 App Router, fundamentally transform how we optimize Core Web Vitals. By running exclusively on the server, they drastically reduce the JavaScript sent to the client.

RSC Benefits for Each Metric

LCP: Instant Rendering

// app/products/[id]/page.tsx
// This component is a Server Component by default

import { getProduct } from '@/lib/api';
import ProductImage from '@/components/ProductImage';

export default async function ProductPage({ params }) {
  // Fetch on the server - no client waterfall
  const product = await getProduct(params.id);
  
  return (
    <article>
      {/* Full HTML sent in the initial response */}
      <ProductImage 
        src={product.image} 
        alt={product.name}
        priority 
      />
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      
      {/* Only this component needs JavaScript */}
      <AddToCartButton productId={product.id} />
    </article>
  );
}

INP: Less JavaScript = Fewer Blocks

// Bundle comparison

// ❌ Traditional approach (Full Client Component)
// Bundle: ~150KB of JavaScript
'use client';
import { useState, useEffect } from 'react';
import { formatPrice, calculateDiscount } from '@/lib/utils';
import { fetchReviews } from '@/lib/api';

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);
  const [reviews, setReviews] = useState([]);
  
  useEffect(() => {
    // Client-side cascade fetches
  }, [productId]);
  
  // All formatting code is sent to the client
}

// ✅ Hybrid RSC approach
// Bundle: ~15KB of JavaScript (only interactivity)

// Server Component - 0KB to client
async function ProductPage({ params }) {
  const [product, reviews] = await Promise.all([
    getProduct(params.id),
    getReviews(params.id),
  ]);
  
  // Formatting executed on server
  const formattedPrice = formatPrice(product.price);
  const discount = calculateDiscount(product);
  
  return (
    <article>
      <h1>{product.name}</h1>
      <p className="price">{formattedPrice}</p>
      
      {/* Only interactive components are Client Components */}
      <InteractiveGallery images={product.images} />
      <AddToCartButton product={product} />
      
      {/* Reviews rendered on server */}
      <ReviewList reviews={reviews} />
    </article>
  );
}

Optimal Composition Pattern

// app/dashboard/page.tsx
import { Suspense } from 'react';
import DashboardHeader from '@/components/DashboardHeader';
import StatsCards from '@/components/StatsCards';
import RecentActivity from '@/components/RecentActivity';
import InteractiveChart from '@/components/InteractiveChart';

export default function DashboardPage() {
  return (
    <div className="dashboard">
      {/* Server Component - instant rendering */}
      <DashboardHeader />
      
      {/* Server Component with data - streaming */}
      <Suspense fallback={<StatsCardsSkeleton />}>
        <StatsCards />
      </Suspense>
      
      {/* Client Component - deferred loading */}
      <Suspense fallback={<ChartSkeleton />}>
        <InteractiveChart />
      </Suspense>
      
      {/* Server Component - parallel streaming */}
      <Suspense fallback={<ActivitySkeleton />}>
        <RecentActivity />
      </Suspense>
    </div>
  );
}

What Tools to Use for Measuring Core Web Vitals?

Accurate measurement requires combining lab (synthetic) data with field (real user) data.

Lab Tools

Lighthouse in Next.js

# Install Lighthouse CI
npm install -g @lhci/cli

# Configuration: lighthouserc.js
module.exports = {
  ci: {
    collect: {
      startServerCommand: 'npm run start',
      url: ['http://localhost:3000/', 'http://localhost:3000/products'],
      numberOfRuns: 3,
    },
    assert: {
      assertions: {
        'categories:performance': ['error', { minScore: 0.9 }],
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
        'interactive': ['error', { maxNumericValue: 3800 }],
      },
    },
    upload: {
      target: 'temporary-public-storage',
    },
  },
};

# Run in CI/CD
lhci autorun

Chrome DevTools Performance

  1. Open DevTools → Performance
  2. Enable "Web Vitals" in the panel
  3. Enable "CPU throttling: 4x slowdown"
  4. Enable "Network: Slow 3G"
  5. Record a user session

Field Tools (RUM)

web-vitals Library

// lib/analytics.ts
import { onLCP, onINP, onCLS, Metric } from 'web-vitals';

function sendToAnalytics(metric: Metric) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    delta: metric.delta,
    id: metric.id,
    navigationType: metric.navigationType,
  });
  
  // Use sendBeacon to avoid blocking unload
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/api/vitals', body);
  } else {
    fetch('/api/vitals', { body, method: 'POST', keepalive: true });
  }
}

export function initWebVitals() {
  onLCP(sendToAnalytics);
  onINP(sendToAnalytics);
  onCLS(sendToAnalytics);
}
// app/layout.tsx
'use client';
import { useEffect } from 'react';
import { initWebVitals } from '@/lib/analytics';

export function WebVitalsReporter() {
  useEffect(() => {
    initWebVitals();
  }, []);
  
  return null;
}

Google Search Console and CrUX

The Chrome User Experience Report (CrUX) provides real user data from millions of Chrome users. Access through:

  • Search Console: Core Web Vitals report by URL/group
  • PageSpeed Insights: CrUX data + Lighthouse diagnostics
  • CrUX Dashboard: Visualizations in Data Studio
  • BigQuery: SQL queries on the public dataset
-- CrUX query in BigQuery
SELECT
  origin,
  p75_lcp,
  p75_inp,
  p75_cls
FROM
  `chrome-ux-report.materialized.metrics_summary`
WHERE
  origin = 'https://yourdomain.com'
  AND yyyymm = 202601

Next.js Analytics (Vercel)

// next.config.js
module.exports = {
  experimental: {
    webVitalsAttribution: ['CLS', 'LCP', 'INP'],
  },
};

// app/layout.tsx
import { SpeedInsights } from '@vercel/speed-insights/next';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <SpeedInsights />
      </body>
    </html>
  );
}

What Are the Core Web Vitals Benchmarks by Site Type?

Google's universal thresholds don't tell the whole story. Different site types have unique challenges and expectations.

E-commerce

Specific Challenges:

  • Product carousels (CLS)
  • Dynamic filters (INP)
  • High-quality images (LCP)

SaaS / Dashboards

Specific Challenges:

  • Complex interactive charts (INP)
  • Real-time updates (CLS)
  • Data-heavy tables (INP, LCP)

Blogs / Content Sites

Specific Challenges:

  • Large hero images (LCP)
  • Ads and embeds (CLS)
  • Lazy loading content (CLS)

Landing Pages

Specific Challenges:

  • Entry animations (CLS)
  • Autoplay videos (LCP)
  • Interactive CTAs (INP)

How to Implement a Continuous Monitoring Pipeline?

Optimizing Core Web Vitals is not a one-time effort. It requires continuous monitoring and early alerts.

GitHub Actions for CI/CD

# .github/workflows/lighthouse.yml
name: Lighthouse CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Build
        run: npm run build
      
      - name: Run Lighthouse
        uses: treosh/lighthouse-ci-action@v11
        with:
          configPath: './lighthouserc.js'
          uploadArtifacts: true
          temporaryPublicStorage: true
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
      
      - name: Comment PR with results
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const results = JSON.parse(
              fs.readFileSync('.lighthouseci/manifest.json', 'utf8')
            );
            // Generate comment with metrics

Custom Monitoring Dashboard

For enterprise projects, our Node.js development team implements monitoring dashboards with:

  • Slack/Email alerts when metrics degrade
  • Historical comparisons by release
  • Segmentation by page type and device
  • Correlation with business metrics

Conclusion: Performance as a Competitive Advantage

Optimizing Core Web Vitals in 2026 requires a deep understanding of the metrics, mastering React and Next.js tools, and maintaining a continuous monitoring process. Sites investing in performance see tangible improvements in SEO, conversions, and user satisfaction.

The techniques covered in this article, from React Server Components to virtualization and INP optimization, represent the state of the art in performant web development. Implementing them correctly requires expertise in frontend architecture and understanding real user behavior.

Does your Next.js site need Core Web Vitals optimization? At KIWOP, we specialize in React development and performance optimization. Contact our team for a free site audit.

Frequently Asked Questions About Core Web Vitals

What Is the Difference Between INP and FID?

FID (First Input Delay) only measured the latency of the first user interaction with the page. INP (Interaction to Next Paint) evaluates all interactions throughout the session and reports a value representative of the worst case (75th percentile). This means INP is a much more demanding and realistic metric of perceived interactivity.

Do Core Web Vitals Directly Affect Google Ranking?

Yes, Core Web Vitals have been a confirmed ranking factor since 2021. However, Google has clarified that relevant and quality content remains more important. Core Web Vitals act as a "tiebreaker" between pages with similar content and as a barrier to entry for premium positions.

How Do React Server Components Affect LCP?

React Server Components significantly improve LCP because the HTML is fully generated on the server and sent in the initial response. There is no JavaScript → fetch → render waterfall on the client. The LCP content is present from the first byte of HTML.

What Is a Good INP Score for E-commerce?

For competitive e-commerce, we recommend an INP below 150ms (Google's "good" threshold is 200ms). Top performers achieve less than 100ms. Critical areas are product filters, carousels, and checkout processes.

Is CLS Measured Throughout the Session or Only on Initial Load?

CLS is measured throughout the page's lifespan, but Google uses "session windows" for the final calculation. A session window is a group of shifts with less than 1 second between them and a maximum duration of 5 seconds. The reported CLS is the maximum of all session windows.

Does Next.js 15 Have Automatic Optimizations for Core Web Vitals?

Yes. Next.js 15 includes: automatic image optimization (WebP/AVIF, lazy loading, dimensions), font optimization (preload, font-display), smart route prefetching, and default React Server Components that reduce JavaScript sent to the client.

How Can I Measure Core Web Vitals from Real Users, Not Just Lighthouse?

Use Google's web-vitals library to capture RUM (Real User Monitoring) metrics. Combine it with your existing analytics stack or services like Vercel Analytics, Datadog RUM, or New Relic Browser. CrUX data in Search Console also shows metrics from real Chrome users.

Is It Worth Optimizing for the 75th Percentile or Should I Aim for the 95th?

Google uses the 75th percentile (p75) for its thresholds, meaning 75% of your users should experience good values. However, for premium sites, optimizing for p90 or p95 ensures a consistent experience even for users in adverse conditions (slow connections, old devices).

Technical
Initial Audit.

AI, security and performance. Diagnosis with phased proposal.

NDA available
Response <24h
Phased proposal

Your first meeting is with a Solutions Architect, not a salesperson.

Request diagnosis