Web Performance Optimization 2026: Core Web Vitals, LCP, CLS, INP Guide

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

Web Performance 2026: Speed is a Feature

Google uses Core Web Vitals as a ranking signal. A 1-second delay reduces conversions by 7%. Performance is not optional — it's product quality.

Core Web Vitals: The Three Metrics That Matter

MetricWhat It MeasuresGoodNeeds WorkPoor
LCPLargest Contentful Paint (loading)≤2.5s≤4s>4s
INPInteraction to Next Paint (responsiveness)≤200ms≤500ms>500ms
CLSCumulative Layout Shift (visual stability)≤0.1≤0.25>0.25

INP replaced FID in March 2024 — it measures all interactions, not just the first.


LCP Optimization: Make the Hero Load Faster

The LCP element is usually a hero image, heading, or video thumbnail.

<!-- 1. Preload your LCP image -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />

<!-- 2. Use fetchpriority on the actual image -->
<img
  src="/hero.webp"
  alt="Hero"
  fetchpriority="high"
  loading="eager"
  decoding="async"
  width="1200"
  height="600"
/>

<!-- 3. DON'T lazy load above-the-fold images -->
<!-- Bad: -->
<img src="/hero.jpg" loading="lazy" />

<!-- 4. Preconnect to image CDN -->
<link rel="preconnect" href="https://cdn.example.com" />
// Next.js: automatic LCP optimization
import Image from 'next/image'

export default function Hero() {
  return (
    <Image
      src="/hero.webp"
      alt="Hero image"
      width={1200}
      height={600}
      priority  // Marks as LCP, adds preload
      sizes="100vw"
    />
  )
}

CLS Prevention: Stop Layout Shifts

/* 1. Always set dimensions on images */
img {
  width: 100%;
  height: auto;
  aspect-ratio: 16/9;  /* Reserve space before load */
}

/* 2. Reserve space for dynamic content */
.ad-slot {
  min-height: 250px;  /* Avoid layout shift when ad loads */
}

/* 3. Use CSS transforms instead of layout properties for animations */
/* BAD: causes reflow */
.bad-animation { width: 100px; animation: expand 0.3s; }
@keyframes expand { to { width: 200px; } }

/* GOOD: GPU-accelerated, no layout impact */
.good-animation { transform: scaleX(0.5); animation: expand 0.3s; }
@keyframes expand { to { transform: scaleX(1); } }
// Next.js Font optimization (eliminates font CLS)
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',  // or 'optional' to avoid FOUT
  preload: true,
})

export default function Layout({ children }) {
  return (
    <html lang="en" className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

INP Optimization: Fast Interactions

INP measures the time from user input to the next visual update.

// 1. Defer non-critical work
function handleClick(e: React.MouseEvent) {
  // Do critical UI update first
  setIsLoading(true)

  // Defer heavy computation
  setTimeout(() => {
    processData()
    setIsLoading(false)
  }, 0)
}

// 2. Use Scheduler API for long tasks
async function processLargeList(items: Item[]) {
  const CHUNK_SIZE = 50

  for (let i = 0; i < items.length; i += CHUNK_SIZE) {
    const chunk = items.slice(i, i + CHUNK_SIZE)
    processChunk(chunk)

    // Yield to browser between chunks
    await new Promise(resolve => setTimeout(resolve, 0))
  }
}

// 3. Use Web Workers for CPU-intensive work
const worker = new Worker('/workers/analysis.js')

function analyzeData(data: number[]) {
  return new Promise(resolve => {
    worker.postMessage(data)
    worker.onmessage = (e) => resolve(e.data)
  })
}

JavaScript Bundle Optimization

// 1. Dynamic imports for code splitting
const HeavyChart = dynamic(() => import('./HeavyChart'), {
  loading: () => <ChartSkeleton />,
  ssr: false,  // Don't render on server
})

// 2. Lazy load based on viewport
import { lazy, Suspense } from 'react'
const Comments = lazy(() => import('./Comments'))

function BlogPost() {
  return (
    <>
      <Article />
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments />  {/* Only loaded when rendered */}
      </Suspense>
    </>
  )
}

// 3. Analyze your bundle
// npm install --save-dev @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})

module.exports = withBundleAnalyzer({
  // ...your config
})

// Run: ANALYZE=true npm run build

Image Optimization

// Serve modern formats with fallbacks
// <picture> element for WebP/AVIF with JPEG fallback

// In Next.js — automatic WebP/AVIF conversion
<Image
  src="/product.jpg"
  alt="Product"
  width={800}
  height={600}
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  quality={75}  // Default 75 is usually fine
/>

// For responsive images outside Next.js
function ResponsiveImage({ src, alt }: { src: string; alt: string }) {
  return (
    <picture>
      <source srcSet={`${src}?format=avif`} type="image/avif" />
      <source srcSet={`${src}?format=webp`} type="image/webp" />
      <img src={src} alt={alt} loading="lazy" decoding="async" />
    </picture>
  )
}

Caching Strategy

// next.config.js — aggressive caching for static assets
const nextConfig = {
  async headers() {
    return [
      {
        source: '/_next/static/(.*)',
        headers: [
          { key: 'Cache-Control', value: 'public, max-age=31536000, immutable' },
        ],
      },
      {
        source: '/images/(.*)',
        headers: [
          { key: 'Cache-Control', value: 'public, max-age=86400, stale-while-revalidate=604800' },
        ],
      },
      {
        source: '/api/(.*)',
        headers: [
          { key: 'Cache-Control', value: 'no-store' },
        ],
      },
    ]
  },
}

Performance Monitoring

// Report Web Vitals to analytics
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals'

function sendToAnalytics({ name, value, id, rating }: Metric) {
  fetch('/api/analytics/vitals', {
    method: 'POST',
    body: JSON.stringify({ name, value: Math.round(value), id, rating }),
    headers: { 'Content-Type': 'application/json' },
    keepalive: true,  // Survives page unload
  })
}

onLCP(sendToAnalytics)
onINP(sendToAnalytics)
onCLS(sendToAnalytics)
onFCP(sendToAnalytics)
onTTFB(sendToAnalytics)

Performance Checklist

OptimizationLCPCLSINPDifficulty
Preload LCP imageEasy
Use next/imageEasy
Use next/fontEasy
Code split routesMedium
Defer third-partyMedium
Web WorkersHard
Server ComponentsMedium

Start with the easy wins — preload, next/image, next/font. These alone can move you from "needs improvement" to "good" on most sites.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro