Executives

undefined

title:: Engineering Core Web Vitals: LCP, INP, and CLS Optimization Patterns description:: A developer's guide to optimizing Core Web Vitals. Covers LCP reduction, INP improvement, CLS elimination with code patterns and measurement tools. focus_keyword:: Core Web Vitals engineering category:: developers author:: Victor Valentine Romo date:: 2026.02.07

Engineering Core Web Vitals: LCP, INP, and CLS Optimization Patterns

Core Web Vitals engineering is the practice of optimizing Largest Contentful Paint, Interaction to Next Paint, and Cumulative Layout Shift through code-level changes rather than content or design modifications. These three metrics are Google ranking signals, but more practically, they measure whether your site feels fast, responsive, and stable to real users.

INP replaced First Input Delay (FID) as the responsiveness metric in March 2024. If your optimization work still targets FID, update your measurement and optimization strategies immediately to address INP's broader scope. INP evaluates all interactions throughout the page lifecycle, not just the first input event, meaning pages that passed FID may fail INP if they have long-running JavaScript tasks that block interaction during active use.

Understanding the Three Metrics

Largest Contentful Paint (LCP)

LCP measures the time from navigation start until the largest content element in the viewport finishes rendering. The target: under 2.5 seconds. The largest element is typically a hero image, video poster, or a large text block.

LCP is not page load time. A page can have elements loading for 10 seconds, but if the largest viewport element renders in 1.8 seconds, LCP passes. This distinction matters for optimization — you're optimizing the critical rendering path for one specific element, not the entire page.

Interaction to Next Paint (INP)

INP measures the latency between a user interaction (click, tap, key press) and the next visual update. Unlike FID, which measured only the first interaction's input delay, INP considers all interactions throughout the page lifecycle and reports the worst one (with some statistical adjustment for pages with many interactions).

Target: under 200 milliseconds. This metric catches the common failure where a page feels fast on initial load but becomes unresponsive during use — long-running JavaScript tasks that block the main thread during interaction.

Cumulative Layout Shift (CLS)

CLS measures unexpected visual movement of visible elements during the page lifecycle. Every time an element shifts position without user interaction, the shift distance and impact area contribute to the CLS score.

Target: under 0.1. CLS failures are the most user-visible performance problem — the text you're reading jumps because an ad loaded above it, or the button you're about to click moves because an image above it rendered with unexpected dimensions.

LCP Optimization Patterns

Pattern 1: Preload the LCP Element

Identify the LCP element on your critical pages using Chrome DevTools Performance panel or Lighthouse. If it's an image, add a preload link in the document :

<link rel="preload" as="image" href="/hero.webp" fetchpriority="high">

The fetchpriority="high" attribute tells the browser to prioritize this resource over other same-priority requests. Without preloading, the browser discovers the image only after parsing the HTML and CSS that references it — adding hundreds of milliseconds to LCP.

Pattern 2: Optimize Server Response Time (TTFB)

LCP cannot be faster than your server response time. If TTFB is 1.5 seconds, your best possible LCP is 1.5 seconds plus rendering time.

Reduce TTFB through: server-side caching (Redis, Varnish), CDN deployment (Cloudflare, Fastly, AWS CloudFront), database query optimization, and edge rendering. For static content, serve pre-generated HTML from CDN edges. For dynamic content, cache aggressively at the application layer.

Pattern 3: Eliminate Render-Blocking Resources

CSS files in the block rendering until they're downloaded and parsed. JavaScript files with neither async nor defer block HTML parsing.

Inline critical CSS — the minimum CSS needed to render above-the-fold content — directly in the . Load the remaining CSS asynchronously:

<link rel="preload" as="style" href="/full.css" onload="this.rel='stylesheet'">

Mark non-critical JavaScript with defer (executes after HTML parsing) or async (executes when ready, order not guaranteed).

Pattern 4: Serve Modern Image Formats

Replace JPEG and PNG with WebP or AVIF. WebP provides 25-35% smaller file sizes than JPEG at equivalent quality. AVIF provides 40-50% savings but has slightly less browser support.

Use for format negotiation:

<picture>
  <source srcset="/hero.avif" type="image/avif">
  <source srcset="/hero.webp" type="image/webp">
  <img src="/hero.jpg" alt="description" width="1200" height="630">
</picture>

Pattern 5: Responsive Images with srcset

Serve appropriately sized images based on viewport width. A 2400px hero image downloaded on a 375px mobile screen wastes bandwidth and slows LCP.

<img
  srcset="/hero-400.webp 400w, /hero-800.webp 800w, /hero-1200.webp 1200w"
  sizes="(max-width: 800px) 100vw, 1200px"
  src="/hero-1200.webp"
  alt="description"
  width="1200"
  height="630"
  fetchpriority="high"
>

INP Optimization Patterns

Pattern 1: Break Up Long Tasks

The browser's main thread runs JavaScript synchronously. A 300ms JavaScript task blocks all interaction for 300ms. The fix: break long tasks into smaller chunks using setTimeout, requestAnimationFrame, or the scheduler.yield() API.

Before:

function processLargeDataset(data) {
  data.forEach(item => heavyComputation(item));
  updateUI();
}

After:

async function processLargeDataset(data) {
  const chunks = chunkArray(data, 50);
  for (const chunk of chunks) {
    chunk.forEach(item => heavyComputation(item));
    await scheduler.yield(); // yields to browser
  }
  updateUI();
}

Pattern 2: Debounce Event Handlers

Input handlers that fire on every keystroke, scroll handlers that trigger layout calculations on every pixel — these create INP failures during active interaction.

Debounce handlers that don't need frame-by-frame precision:

function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

searchInput.addEventListener('input', debounce(handleSearch, 200));

Pattern 3: Offload Computation to Web Workers

Heavy computation — data parsing, sorting large arrays, cryptographic operations — should run in Web Workers rather than on the main thread. Web Workers execute in a separate thread, leaving the main thread free to respond to user interactions.

const worker = new Worker('/compute-worker.js');
worker.postMessage({ data: largeDataset });
worker.onmessage = (event) => {
  renderResults(event.data);
};

Pattern 4: Optimize Third-Party Scripts

Analytics, chat widgets, A/B testing tools, and advertising scripts frequently cause INP failures. Audit third-party scripts with Chrome DevTools Performance panel — filter the flame chart by third-party domain.

Load non-critical third-party scripts with async and defer. Delay non-essential scripts until after user interaction:

document.addEventListener('scroll', () => {
  loadChatWidget();
  loadAnalytics();
}, { once: true });

Pattern 5: Use CSS Containment

CSS contain property tells the browser that an element's rendering is independent of the rest of the page. This enables rendering optimizations that reduce repaint costs during interaction.

.card {
  contain: layout style paint;
}
content-visibility: auto goes further — the browser skips rendering off-screen elements entirely:
.below-fold-section {
  content-visibility: auto;
  contain-intrinsic-size: auto 500px;
}

CLS Optimization Patterns

Pattern 1: Explicit Dimensions on Media

Every , , and