Skip to main content
{/}codefunc
GuidesJuly 7, 2026 · 9 min read

Web Performance and Core Web Vitals: What to Measure and How to Fix It

A practical guide to frontend performance — LCP, INP, CLS explained, with optimization techniques for images, JavaScript, fonts, and rendering that improve both user experience and search rankings.

By codefunc

performancecore-web-vitalslcpinpclsoptimizationseo

Performance is user experience measured in milliseconds. A page that loads in 1 second feels instant. A page that loads in 4 seconds feels broken. Google measures this too — Core Web Vitals are ranking factors. But performance optimization is not about gaming metrics; it is about respecting users' time and device capabilities.

Bottom line: Core Web Vitals measure loading (LCP), interactivity (INP), and visual stability (CLS). Fix LCP by optimizing images and critical resources. Fix INP by reducing JavaScript work and breaking up long tasks. Fix CLS by reserving space for dynamic content. Measure with real user data, not just lab tests.

Core Web Vitals explained

Google's Core Web Vitals are three metrics that represent user experience:

Metric Measures Good Poor
LCP (Largest Contentful Paint) Loading ≤ 2.5s > 4s
INP (Interaction to Next Paint) Interactivity ≤ 200ms > 500ms
CLS (Cumulative Layout Shift) Visual stability ≤ 0.1 > 0.25

These affect search rankings and user behavior. Users abandon slow pages.

LCP: Largest Contentful Paint

LCP measures when the largest visible content element renders — usually a hero image, main heading, or large text block.

What triggers LCP:

  • <img> elements
  • <image> inside SVG
  • Video poster images
  • Background images via CSS
  • Block-level text elements

Common LCP problems:

  • Slow server response (TTFB)
  • Render-blocking CSS/JS
  • Unoptimized images
  • Client-side rendering delays

INP: Interaction to Next Paint

INP replaced FID (First Input Delay) in 2024. It measures responsiveness throughout the page lifetime, not just first interaction.

What INP measures:

  • Time from user input (click, tap, key press) to next frame paint
  • Worst interaction latency during page visit
  • 75th percentile across all users

Common INP problems:

  • Long JavaScript tasks (>50ms)
  • Heavy event handlers
  • Forced synchronous layouts
  • Third-party scripts blocking main thread

CLS: Cumulative Layout Shift

CLS measures unexpected movement of visible content. Every time an element shifts without user interaction, it accumulates.

What causes layout shifts:

  • Images without dimensions
  • Ads/embeds without reserved space
  • Web fonts causing text reflow (FOIT/FOUT)
  • Dynamically injected content

Measuring performance

Lab tools (development)

Tool Use for
Lighthouse Comprehensive audit in DevTools
DevTools Performance panel Flame charts, detailed timing
WebPageTest Filmstrip, waterfalls, comparison
PageSpeed Insights Lab + field data combined

Field tools (real users)

Tool Use for
Chrome UX Report (CrUX) Real user data at origin level
Search Console Core Web Vitals report per URL
Web Vitals library Custom RUM implementation

Important: Lab and field results differ. Lab tests use consistent conditions; field data reflects real devices, networks, and usage patterns. Optimize for field data.

The web-vitals library

import { onLCP, onINP, onCLS } from 'web-vitals';

onLCP(console.log);
onINP(console.log);
onCLS(console.log);

Send to your analytics for real user monitoring (RUM).

Optimizing LCP

1. Optimize server response time (TTFB)

Technique Impact
Use a CDN Serve from edge locations
Cache HTML Static pages, stale-while-revalidate
Optimize backend Database queries, API calls
Use HTTP/2 or HTTP/3 Multiplexing, header compression

For static sites, TTFB is usually fast. Dynamic SSR needs more attention.

2. Eliminate render-blocking resources

<!-- Defer non-critical JS -->
<script src="analytics.js" defer></script>

<!-- Async for independent scripts -->
<script src="widget.js" async></script>

<!-- Inline critical CSS -->
<style>
  /* Above-the-fold styles */
</style>

<!-- Load full CSS asynchronously -->
<link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">

3. Optimize images

Images are the most common LCP element.

Technique Implementation
Modern formats WebP, AVIF instead of PNG/JPEG
Responsive images srcset and sizes attributes
Lazy loading loading="lazy" for below-fold
Preload hero image <link rel="preload" as="image">
Proper sizing Don't serve 4000px image for 400px slot
<!-- Responsive image with modern format -->
<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img 
    src="hero.jpg" 
    alt="Hero image" 
    width="1200" 
    height="630"
    fetchpriority="high"
  >
</picture>

4. Preload critical resources

<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/hero.webp" as="image">
<link rel="preconnect" href="https://api.example.com">

5. Avoid client-side rendering for LCP content

If your LCP element depends on JavaScript to render, users wait for:

  1. HTML download
  2. JS download
  3. JS parse and execute
  4. Data fetch (maybe)
  5. Render

Use SSR or SSG for LCP content. Hydrate interactivity after.

Optimizing INP

1. Break up long tasks

The browser's main thread handles JavaScript, layout, paint, and user input. Long tasks (>50ms) block everything.

// Bad: one long task
function processLargeArray(items) {
  items.forEach(item => heavyProcessing(item));
}

// Better: yield to main thread
async function processLargeArray(items) {
  for (const item of items) {
    heavyProcessing(item);
    // Yield every 5ms
    if (performance.now() - start > 5) {
      await scheduler.yield(); // or setTimeout
      start = performance.now();
    }
  }
}

2. Use requestIdleCallback for non-urgent work

requestIdleCallback(() => {
  // Analytics, prefetching, non-critical updates
  sendAnalytics();
});

3. Debounce expensive handlers

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

input.addEventListener('input', debounce(handleSearch, 150));

4. Avoid forced synchronous layout

// Bad: read then write in loop (layout thrashing)
elements.forEach(el => {
  const height = el.offsetHeight; // read (forces layout)
  el.style.height = height + 10 + 'px'; // write
});

// Good: batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight); // all reads
elements.forEach((el, i) => {
  el.style.height = heights[i] + 10 + 'px'; // all writes
});

5. Minimize third-party impact

Third-party scripts (ads, analytics, widgets) often cause INP issues.

Technique Implementation
Load async/defer <script async>
Use facade patterns Load on interaction
Set resource hints <link rel="preconnect">
Audit regularly Lighthouse third-party summary

Optimizing CLS

1. Always set image dimensions

<!-- CLS-safe image -->
<img src="photo.jpg" alt="..." width="800" height="600">

<!-- Or use aspect-ratio in CSS -->
<style>
  .thumbnail {
    aspect-ratio: 16 / 9;
    width: 100%;
  }
</style>

2. Reserve space for ads and embeds

.ad-slot {
  min-height: 250px;
  background: var(--muted);
}

.video-embed {
  aspect-ratio: 16 / 9;
}

3. Optimize web font loading

/* Prevent FOIT (invisible text) */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: swap; /* Show fallback immediately */
}

Better yet, use font-display: optional for non-critical fonts — they only show if already cached.

4. Avoid inserting content above existing content

// Bad: prepend shifts everything down
container.prepend(newElement);

// Better: append to end or use fixed position
container.append(newElement);

// Or animate in with transforms (no layout shift)
newElement.style.transform = 'translateY(-100%)';
requestAnimationFrame(() => {
  newElement.style.transform = 'translateY(0)';
});

5. Use CSS contain for isolated components

.card {
  contain: layout; /* Layout changes don't affect siblings */
}

JavaScript optimization

Code splitting

Load only what's needed for the current page:

// Dynamic import
const module = await import('./heavy-feature.js');

// React lazy loading
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

Tree shaking

Import only what you use:

// Bad: imports entire library
import _ from 'lodash';

// Good: imports only needed function
import debounce from 'lodash/debounce';

Minification

Reduce file size by removing whitespace, shortening names, and eliminating dead code.

For quick minification of individual files, use:

Production builds should use bundler minification (Vite, webpack, esbuild).

Image optimization checklist

  • Use modern formats (WebP, AVIF)
  • Serve responsive sizes with srcset
  • Set width and height attributes
  • Lazy load below-fold images
  • Preload LCP image
  • Compress appropriately (quality vs size)
  • Use CDN with image optimization

For small icons and inline images, base64 encoding can reduce HTTP requests. Use the Image Base64 Encoder for assets under 10KB.

Performance budgets

Set limits and enforce them in CI:

Metric Budget
Total JS < 200KB gzipped
Total CSS < 50KB gzipped
LCP < 2.5s
INP < 200ms
CLS < 0.1
Total page weight < 1MB

Tools like Lighthouse CI can fail builds that exceed budgets.

Monitoring and alerting

Real User Monitoring setup

import { onLCP, onINP, onCLS } from 'web-vitals';

function sendToAnalytics(metric) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    delta: metric.delta,
    id: metric.id,
    page: window.location.pathname,
  });
  
  navigator.sendBeacon('/analytics', body);
}

onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);

Search Console monitoring

Check Core Web Vitals report weekly:

  • URL-level pass/fail status
  • Trends over time
  • Mobile vs desktop breakdown

Framework-specific tips

Next.js

  • Use next/image for automatic optimization
  • Leverage SSG/ISR for fast TTFB
  • Use next/font for optimized font loading
  • Enable experimental.optimizeCss

React

  • Use React.lazy() for code splitting
  • Memoize expensive components
  • Use useDeferredValue for non-urgent updates
  • Profile with React DevTools

Static sites

  • Pre-render all pages (SSG)
  • Inline critical CSS
  • Preload key resources
  • Use service worker for repeat visits

Performance debugging workflow

  1. Measure — Lighthouse, CrUX, Search Console
  2. Identify — Which metric is failing? Which URL?
  3. Diagnose — DevTools Performance panel, waterfall
  4. Fix — Apply targeted optimization
  5. Verify — Re-measure in lab and monitor field data
  6. Prevent — Add to performance budget, CI checks

Practical takeaway

Performance optimization is iterative. Start with the biggest impact: fix LCP with image optimization and critical resource loading. Fix INP by auditing JavaScript and breaking up long tasks. Fix CLS by setting explicit dimensions. Measure with real user data, not just lab tests. Set budgets and enforce them in CI. Performance is not a one-time project — it is an ongoing practice.

Sources

Try it on codefunc

Related articles

3

Search tools

Find a developer tool