Ink&Horizon
HomeBlogTutorialsLanguages
Ink&Horizon— where knowledge meets the horizon —Learn to build exceptional software. Tutorials, guides, and references for developers — from first brushstroke to masterwork.

Learn

  • Blog
  • Tutorials
  • Languages

Company

  • About Us
  • Contact Us
  • Privacy Policy

Account

  • Sign In
  • Register
  • Profile
Ink & Horizon

© 2026 InkAndHorizon. All rights reserved.

Privacy PolicyTerms of Service
Back to Blog
Frontend

Web Performance in 2026: Core Web Vitals, INP & Modern Optimization

Master LCP, INP, CLS, View Transitions, Speculation Rules, and the metrics that actually affect Google rankings

2026-03-18 23 min read
ContentsCore Web Vitals 2026: The Three Metrics That MatterLCP: Fixing the Largest Contentful PaintINP: The New Responsiveness MetricCLS: Eliminating Layout ShiftsView Transitions API: Smooth Page NavigationsSpeculation Rules API: Instant NavigationsImage Optimization: The Biggest Performance WinJavaScript Bundle OptimizationMeasuring Performance: Real User MonitoringKey Takeaways

Core Web Vitals 2026: The Three Metrics That Matter

Core Web Vitals are the performance metrics Google uses as ranking signals. In 2026, the three metrics are: Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS). INP replaced First Input Delay (FID) in March 2024 — this is a critical change for interview prep.

LCP measures loading speed (how fast the largest piece of content appears). INP measures responsiveness (how quickly the page responds to ALL user interactions, not just the first). CLS measures visual stability (how much the layout shifts during loading).

The thresholds: LCP < 2.5s (good), INP < 200ms (good), CLS < 0.1 (good). These are measured at the 75th percentile of all page views — meaning 75% of your users must have a good experience, not just the median.

Key Takeaways

LCP < 2.5s: Time until the largest visible element renders.
INP < 200ms: Worst interaction latency across the entire page visit.
CLS < 0.1: Total unexpected layout shift score.
INP replaced FID in March 2024 — FID only measured the first interaction.
Measured at 75th percentile — 75% of real users must pass.

LCP: Fixing the Largest Contentful Paint

LCP typically refers to the largest image, video poster, or text block visible in the viewport. The most common LCP culprits are: hero images loaded lazily (they should be eager-loaded), web fonts blocking text rendering, server response time (TTFB), and render-blocking CSS/JS.

The #1 LCP optimization most developers miss: use fetchpriority="high" on the LCP image. This tells the browser to prioritize downloading it above other resources. Combined with preloading, it can reduce LCP by 20-40%.

For text-based LCP elements, use font-display: swap in your @font-face declarations to prevent invisible text while custom fonts load.

Snippet
<!-- ✅ LCP Image Optimization -->
<!-- 1. Preload the LCP image in <head> -->
<link 
  rel="preload" 
  as="image" 
  href="/hero-image.webp"
  fetchpriority="high"
  type="image/webp"
/>

<!-- 2. Eager load with fetchpriority="high" -->
<img 
  src="/hero-image.webp" 
  alt="Hero banner"
  width="1200" 
  height="600"
  fetchpriority="high"
  loading="eager"
  decoding="async"
/>

<!-- ❌ Common mistake: lazy loading the LCP image -->
<img src="/hero.webp" loading="lazy" /> <!-- NEVER lazy-load the LCP element! -->

<!-- ✅ Next.js: priority prop on next/image -->
import Image from 'next/image';
<Image 
  src="/hero.webp"
  alt="Hero"
  width={1200}
  height={600}
  priority  // Sets fetchpriority="high" + eager loading
/>

Key Takeaways

fetchpriority="high" on the LCP element is the single biggest LCP win.
NEVER lazy-load the LCP image — only elements below the fold.
Preload the LCP image in <head> for earliest possible download.
Use WebP/AVIF formats — 25-50% smaller than JPEG.
In Next.js, use the priority prop on the hero Image component.

INP: The New Responsiveness Metric

INP (Interaction to Next Paint) replaced FID as a Core Web Vital in March 2024. While FID only measured the delay before the FIRST interaction, INP measures the worst-case response time across ALL interactions during the entire page visit.

INP counts three phases: Input Delay (time before event handler runs — blocked by main thread work), Processing Time (event handler execution time), and Presentation Delay (time to calculate style/layout and paint the next frame).

The biggest INP killers are: long JavaScript tasks that block the main thread, expensive React re-renders, synchronous layout calculations (forced reflows), and large DOM trees that slow down style calculations.

Snippet
// ❌ Bad: Long task blocks main thread → poor INP
button.addEventListener('click', () => {
  // Synchronous heavy computation — blocks UI for 500ms
  const result = heavyComputation(data); // 500ms blocking!
  updateUI(result);
});

// ✅ Fix 1: Break up long tasks with scheduler.yield()
button.addEventListener('click', async () => {
  const chunk1 = processFirstHalf(data);
  await scheduler.yield(); // Give browser a chance to paint
  const chunk2 = processSecondHalf(data);
  await scheduler.yield();
  updateUI([...chunk1, ...chunk2]);
});

// ✅ Fix 2: Use requestIdleCallback for non-urgent work
button.addEventListener('click', () => {
  // Immediate visual feedback
  showLoadingSpinner();
  
  // Defer heavy work to idle time
  requestIdleCallback(() => {
    const result = heavyComputation(data);
    updateUI(result);
    hideLoadingSpinner();
  });
});

// ✅ Fix 3: Move heavy computation to a Web Worker
const worker = new Worker('/compute-worker.js');
button.addEventListener('click', () => {
  showLoadingSpinner();
  worker.postMessage({ data });
});
worker.onmessage = (e) => {
  updateUI(e.data.result);
  hideLoadingSpinner();
};

CLS: Eliminating Layout Shifts

CLS measures how much your page layout shifts unexpectedly. Every time an element changes position after it has been painted to the screen (without user interaction triggering it), it contributes to the CLS score. A score above 0.1 is considered poor.

The top CLS causes: images without width/height attributes, dynamically injected content (ads, banners), web fonts causing text reflow (FOUT), and late-loading CSS that changes element sizes.

The fix is always the same: reserve space for content before it loads. Set explicit width and height on images and videos, use aspect-ratio CSS, and use min-height for dynamic content areas.

Snippet
/* ✅ Reserve space for images — prevents layout shift */
img, video {
  /* Modern approach: aspect-ratio from HTML attributes */
  aspect-ratio: attr(width) / attr(height);
  width: 100%;
  height: auto;
}

/* ✅ Skeleton for dynamic content — reserves exact space */
.ad-slot {
  min-height: 250px; /* Reserve space before ad loads */
  background: #f0f0f0;
  contain: layout; /* Prevents this element from shifting others */
}

/* ✅ Font-display: optional — prevents ANY text reflow */
@font-face {
  font-family: 'CustomFont';
  src: url('/fonts/custom.woff2') format('woff2');
  font-display: optional; /* If font isn't cached, use system font — zero CLS */
}

/* ✅ CSS containment — limits layout recalculation scope */
.card {
  contain: layout style paint; /* Changes inside .card can't affect outside layout */
  content-visibility: auto;    /* Skip rendering for off-screen cards */
}

Key Takeaways

Always set width and height on <img> and <video> elements.
Use min-height for dynamic content slots (ads, embeds, notifications).
font-display: optional gives zero CLS — uses cached font or system font.
CSS contain: layout prevents an element's changes from shifting siblings.
content-visibility: auto skips rendering off-screen elements entirely.

View Transitions API: Smooth Page Navigations

The View Transitions API (stable in Chrome, Safari 18+) enables smooth animated transitions between page states — both for single-page apps (SPA) and multi-page apps (MPA). It creates a snapshot of the current DOM, applies your changes, then animates between the old and new states.

This replaces the need for complex animation libraries for page transitions. The browser handles snapshotting, cross-fading, and even morphing shared elements between pages. It works beautifully with Next.js App Router navigation.

For MPAs, the API works across full page navigations with a few lines of CSS. For SPAs, you wrap state changes in document.startViewTransition() to animate between states.

Snippet
// SPA: Animate between states
async function navigateTo(url) {
  // Check browser support
  if (!document.startViewTransition) {
    return updateDOM(url); // Fallback: no animation
  }

  const transition = document.startViewTransition(async () => {
    await updateDOM(url);
  });

  // Wait for transition to complete
  await transition.finished;
}

/* CSS: Define transition animations */
/* Default cross-fade (works out of the box) */
::view-transition-old(root) {
  animation: fade-out 0.3s ease-out;
}
::view-transition-new(root) {
  animation: fade-in 0.3s ease-in;
}

/* Shared element transition — morphs between pages */
/* Old page */
.blog-card { view-transition-name: blog-hero; }

/* New page */
.blog-hero-image { view-transition-name: blog-hero; }

/* The browser automatically morphs blog-card → blog-hero-image */
::view-transition-group(blog-hero) {
  animation-duration: 0.4s;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

Speculation Rules API: Instant Navigations

The Speculation Rules API (Chrome 121+) lets browsers prefetch or prerender pages that users are likely to navigate to. When the user actually clicks, the page loads instantly — literally 0ms perceived load time because it was already rendered in a hidden tab.

This is different from <link rel="prefetch"> which only downloads the HTML. Speculation Rules with "prerender" actually executes JavaScript and renders the page in a hidden browsing context, making it instant on navigation.

You can define rules based on URL patterns, CSS selectors (e.g., "prerender all links with class .nav-link"), or eagerness levels (immediate, eager, moderate, conservative).

Snippet
<!-- Speculation Rules: Prerender pages for instant navigation -->
<script type="speculationrules">
{
  "prerender": [
    {
      "where": { "href_matches": "/blog/*" },
      "eagerness": "moderate"
    }
  ],
  "prefetch": [
    {
      "where": { "selector_matches": ".nav-link" },
      "eagerness": "eager" 
    },
    {
      "urls": ["/about", "/blog", "/contact"]
    }
  ]
}
</script>

<!-- Eagerness levels:
  - "immediate": Speculate as soon as rules are observed
  - "eager": Speculate at the earliest opportunity (same as immediate currently)
  - "moderate": Speculate on pointer hover (200ms delay)
  - "conservative": Speculate only on pointer down (mouse click start)
-->

<!-- Next.js: Use next/link with prefetch -->
import Link from 'next/link';
// Next.js automatically prefetches visible Link destinations
<Link href="/blog" prefetch={true}>Blog</Link>

Key Takeaways

prerender = full page render in hidden tab → 0ms navigation.
prefetch = download HTML only → faster but not instant.
Eagerness levels control when speculation starts (hover vs click).
"moderate" is the sweet spot — speculates on hover, saves bandwidth.
Next.js <Link> already prefetches on viewport entry — add Speculation Rules on top for prerendering.

Image Optimization: The Biggest Performance Win

Images typically account for 50-70% of total page weight. Optimizing them is the single biggest performance win for most websites. The modern stack: WebP for broad compatibility, AVIF for maximum compression, and responsive srcset for serving the right size.

The key rule: never serve an image larger than it appears on screen. A 4000px hero image displayed at 1200px wastes 90% of the bytes. Use srcset with sizes attribute to let the browser pick the best size for the user's device.

For Next.js, next/image handles all of this automatically: responsive sizes, WebP/AVIF conversion, lazy loading, and blur placeholders.

Snippet
<!-- ✅ Responsive images with srcset and sizes -->
<img 
  src="/blog/hero-800.webp"
  srcset="
    /blog/hero-400.webp 400w,
    /blog/hero-800.webp 800w,
    /blog/hero-1200.webp 1200w,
    /blog/hero-1600.webp 1600w
  "
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 80vw, 1200px"
  alt="Blog hero image"
  width="1200"
  height="600"
  loading="lazy"
  decoding="async"
/>

<!-- ✅ AVIF with WebP and JPEG fallbacks -->
<picture>
  <source srcset="/hero.avif" type="image/avif" />
  <source srcset="/hero.webp" type="image/webp" />
  <img src="/hero.jpg" alt="Hero" width="1200" height="600" />
</picture>

<!-- ✅ Next.js Image — handles everything automatically -->
import Image from 'next/image';
<Image
  src="/blog/hero.jpg"
  alt="Blog hero"
  width={1200}
  height={600}
  sizes="(max-width: 640px) 100vw, 80vw"
  placeholder="blur"
  blurDataURL="data:image/jpeg;base64,..."
/>

Key Takeaways

AVIF = 50% smaller than JPEG. WebP = 25-35% smaller than JPEG.
srcset + sizes lets the browser choose the optimal image size.
Never serve images larger than their display size.
Use loading="lazy" for below-the-fold images, never for LCP.
Next.js Image auto-converts to WebP, adds srcset, lazy loads, and blur placeholder.

JavaScript Bundle Optimization

After images, JavaScript is the biggest performance bottleneck. Every KB of JS must be downloaded, parsed, compiled, and executed — making it far more expensive than equivalent bytes of HTML, CSS, or images.

The target for production: < 100KB of JavaScript for the initial page load (first load JS in Next.js). Anything above 200KB should raise red flags.

Key strategies: code splitting (load only what the current route needs), tree shaking (remove unused exports), dynamic imports (lazy-load heavy components), and replacing heavy libraries with lighter alternatives.

Snippet
// ✅ Dynamic import — lazy load heavy components
import dynamic from 'next/dynamic';

const CodeEditor = dynamic(() => import('./CodeEditor'), {
  loading: () => <div className="editor-skeleton" />,
  ssr: false, // Don't server-render (uses browser APIs)
});

// ✅ Route-based code splitting (automatic in Next.js App Router)
// Each page.tsx is automatically a separate bundle

// ✅ Replace heavy libraries with lighter alternatives
// ❌ moment.js (67KB gzipped) → ✅ date-fns (tree-shakeable, ~3KB per function)
// ❌ lodash (71KB) → ✅ lodash-es (tree-shakeable) or native methods
// ❌ axios (13KB) → ✅ native fetch() (0KB — built into browser & Node 22)

// ✅ Analyze your bundle
// next.config.ts
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer(nextConfig);
// Run: ANALYZE=true next build

Measuring Performance: Real User Monitoring

Lab tools (Lighthouse, PageSpeed Insights) test in controlled conditions. Field data (Chrome UX Report, web-vitals library) measures real users on real devices and networks. Google uses FIELD data for rankings, not lab data.

Always measure with the web-vitals library in production to get real Core Web Vitals data from your actual users. Send it to your analytics service to identify pages and interactions that need optimization.

Use Chrome DevTools Performance panel to diagnose issues: look for Long Tasks (>50ms) blocking the main thread, layout shifts, and slow network requests.

Snippet
// ✅ Measure real user Core Web Vitals
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals';

function sendToAnalytics(metric) {
  // Send to your analytics service
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating, // 'good', 'needs-improvement', 'poor'
    delta: metric.delta,
    id: metric.id,
    page: window.location.pathname,
    // Attribution data — tells you WHAT caused the issue
    ...(metric.attribution && {
      element: metric.attribution.element,
      largestShiftTarget: metric.attribution.largestShiftTarget,
    }),
  });

  // Use sendBeacon for reliability (fires even on page unload)
  navigator.sendBeacon('/api/analytics', body);
}

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

Key Takeaways

Google uses field data (real users), not lab data, for ranking decisions.
web-vitals library is the official way to measure Core Web Vitals in production.
sendBeacon ensures analytics fire even if the user navigates away.
Attribution data tells you exactly WHICH element caused a bad score.
Chrome UX Report (CrUX) provides 28-day rolling field data for your origin.

Key Takeaways

Web performance in 2026 revolves around three Core Web Vitals (LCP, INP, CLS), modern APIs (View Transitions, Speculation Rules), and fundamental practices (image optimization, bundle splitting, real user monitoring).

The highest-impact optimizations: fetchpriority="high" on LCP images, breaking long tasks with scheduler.yield() for INP, setting width/height on all images for CLS, using next/image for automatic optimization, and Speculation Rules for instant navigations.

For interviews, understand: why INP replaced FID, how View Transitions work, what Speculation Rules prerender vs prefetch, and how to use the web-vitals library for real user monitoring.

Key Takeaways

LCP < 2.5s, INP < 200ms, CLS < 0.1 — at the 75th percentile.
INP replaced FID in 2024 — measures ALL interactions, not just the first.
fetchpriority="high" on LCP images is the biggest single LCP win.
scheduler.yield() breaks long tasks for better INP.
View Transitions API enables smooth animated page navigations.
Speculation Rules prerender pages for 0ms perceived navigation.
Measure with web-vitals in production — Google ranks on field data.
AS
Article Author
Ashutosh
Lead Developer

Related Knowledge

Tutorial

React 19 Hooks Mastery

5m read
Tutorial

Next.js 15 — The Complete Guide

5m read
Tutorial

JavaScript Fundamentals

5m read
Article

Understanding Closures in JavaScript: The Complete 2026 Guide

22 min read
Article

React 19 Server Components: The Definitive 2026 Guide

28 min read
Article

Next.js 15 App Router Masterclass: Everything You Need to Know

25 min read