Core Web Vitals: How to Optimize Your Website Performance

THEJORD Team8 min read
performanceseoweb-development

Complete guide to Google Core Web Vitals. LCP, FID, CLS explained with practical examples and optimization techniques.

Core Web Vitals: How to Optimize Your Website Performance

Understanding Core Web Vitals

Core Web Vitals are Google's standardized metrics for measuring user experience on websites. Since 2021, they've been a ranking factor in search results, making them essential for both SEO and user satisfaction.

This guide explains what each metric measures, why it matters, and provides practical techniques to optimize your website's performance.

The Three Core Web Vitals

Largest Contentful Paint (LCP)

LCP measures how long it takes for the largest visible content element to render. This is typically a hero image, video thumbnail, or large text block.

  • Good: ≤2.5 seconds
  • Needs Improvement: 2.5-4 seconds
  • Poor: >4 seconds

LCP reflects perceived load speed—it's what users "feel" as the page becoming ready.

Interaction to Next Paint (INP)

INP replaced First Input Delay (FID) in March 2024. It measures the responsiveness of a page to user interactions throughout the entire session, not just the first click.

  • Good: ≤200 milliseconds
  • Needs Improvement: 200-500 milliseconds
  • Poor: >500 milliseconds

INP captures how snappy your page feels when users click buttons, tap links, or type in inputs.

Cumulative Layout Shift (CLS)

CLS measures visual stability—how much elements unexpectedly move around as the page loads.

  • Good: ≤0.1
  • Needs Improvement: 0.1-0.25
  • Poor: >0.25

CLS quantifies the frustrating experience of clicking a button that suddenly moves, or text jumping as ads load.

Measuring Core Web Vitals

Lab Tools

  • Chrome DevTools Performance panel: Detailed timeline analysis
  • Lighthouse: Automated audits with scores and suggestions
  • WebPageTest: Multi-location testing with waterfalls
  • PageSpeed Insights: Combined lab and field data

Field Data (Real Users)

  • Chrome User Experience Report (CrUX): Real-world data from Chrome users
  • Google Search Console: Core Web Vitals report for your site
  • web-vitals JavaScript library: Collect your own field data

Implementation Example

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

function sendToAnalytics({name, delta, id}) {
  // Send metric to your analytics endpoint
  fetch('/analytics', {
    method: 'POST',
    body: JSON.stringify({name, delta, id}),
    headers: {'Content-Type': 'application/json'}
  });
}

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

Optimizing Largest Contentful Paint

Identify the LCP Element

First, find what's being measured. In Chrome DevTools:

  1. Open Performance panel
  2. Record a page load
  3. Look for "LCP" marker in the timeline
  4. Check what element triggered it

Optimize Images

If the LCP element is an image:

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

<!-- Use modern formats -->
<picture>
  <source srcset="/hero.avif" type="image/avif">
  <source srcset="/hero.webp" type="image/webp">
  <img src="/hero.jpg" alt="Hero" fetchpriority="high">
</picture>

For detailed image optimization techniques, check our guide on optimizing images for the web.

Eliminate Render-Blocking Resources

<!-- Inline critical CSS -->
<style>
  /* Only styles needed for above-the-fold content */
  .hero { ... }
  .nav { ... }
</style>

<!-- Defer non-critical CSS -->
<link
  rel="preload"
  href="/styles.css"
  as="style"
  onload="this.onload=null;this.rel='stylesheet'"
>

<!-- Defer JavaScript -->
<script src="/app.js" defer></script>

Server Response Time

Reduce Time to First Byte (TTFB):

  • Use CDN for static assets
  • Enable server-side caching
  • Optimize database queries
  • Consider edge rendering (Cloudflare Workers, Vercel Edge)
# nginx caching example
location ~* \.(jpg|jpeg|png|gif|webp|avif|css|js)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

Font Loading

Fonts can delay LCP if text is the largest element:

<!-- Preload critical fonts -->
<link
  rel="preload"
  href="/fonts/inter.woff2"
  as="font"
  type="font/woff2"
  crossorigin
>

<!-- Use font-display: swap -->
<style>
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: swap;
}
</style>

Optimizing Interaction to Next Paint

Identify Slow Interactions

Use Chrome DevTools Performance panel:

  1. Record a session with interactions
  2. Look for long tasks (red corners)
  3. Find interactions with high latency

Reduce JavaScript Execution Time

// Bad: Long synchronous task
function processData(largeArray) {
  largeArray.forEach(item => heavyProcessing(item));
}

// Good: Break into smaller chunks
async function processDataChunked(largeArray) {
  const chunkSize = 100;
  for (let i = 0; i < largeArray.length; i += chunkSize) {
    const chunk = largeArray.slice(i, i + chunkSize);
    chunk.forEach(item => heavyProcessing(item));
    // Yield to main thread
    await new Promise(resolve => setTimeout(resolve, 0));
  }
}

Use Web Workers

Move heavy computation off the main thread:

// main.js
const worker = new Worker('/worker.js');

worker.postMessage({data: largeDataset});
worker.onmessage = (e) => {
  const result = e.data;
  updateUI(result);
};

// worker.js
self.onmessage = (e) => {
  const result = heavyProcessing(e.data);
  self.postMessage(result);
};

Debounce Input Handlers

// Bad: Runs on every keystroke
input.addEventListener('input', (e) => {
  expensiveSearch(e.target.value);
});

// Good: Debounced
let timeout;
input.addEventListener('input', (e) => {
  clearTimeout(timeout);
  timeout = setTimeout(() => {
    expensiveSearch(e.target.value);
  }, 150);
});

Use requestIdleCallback

// Defer non-urgent work
requestIdleCallback(() => {
  // Analytics, prefetching, etc.
  trackPageView();
  prefetchNextPage();
}, { timeout: 2000 });

Optimizing Cumulative Layout Shift

Always Include Dimensions

<!-- Always specify width and height -->
<img
  src="photo.jpg"
  width="800"
  height="600"
  alt="Description"
>

<!-- For responsive images, use aspect-ratio -->
<style>
.responsive-image {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}
</style>

Reserve Space for Ads

<style>
.ad-container {
  min-height: 250px; /* Reserve space */
  background: #f0f0f0;
}
</style>

<div class="ad-container">
  <!-- Ad loads here -->
</div>

Avoid Inserting Content Above Existing Content

// Bad: Prepending shifts everything down
container.insertBefore(newElement, container.firstChild);

// Better: Add below existing content
container.appendChild(newElement);

// Or: Use CSS to avoid shift
.notification {
  position: fixed;
  top: 0;
  /* Doesn't affect layout */
}

Font Loading Without Shift

<style>
/* Use size-adjust for fallback font */
@font-face {
  font-family: 'Custom Font';
  src: url('/custom.woff2') format('woff2');
  font-display: swap;
}

body {
  font-family: 'Custom Font', -apple-system, BlinkMacSystemFont, sans-serif;
}

/* Match fallback metrics to custom font */
@font-face {
  font-family: 'Fallback';
  src: local('Arial');
  size-adjust: 105%;
  ascent-override: 90%;
}
</style>

Animate with transform and opacity

/* Bad: Animating width/height causes layout shift */
.expand {
  animation: expandBad 0.3s;
}
@keyframes expandBad {
  from { height: 0; }
  to { height: 100px; }
}

/* Good: Use transform instead */
.expand {
  animation: expandGood 0.3s;
}
@keyframes expandGood {
  from { transform: scaleY(0); }
  to { transform: scaleY(1); }
}

Framework-Specific Optimizations

React

// Use React.lazy for code splitting
const HeavyComponent = React.lazy(() =>
  import('./HeavyComponent')
);

// Placeholder while loading
<Suspense fallback={<Skeleton />}>
  <HeavyComponent />
</Suspense>

// Use useDeferredValue for non-urgent updates
const deferredSearch = useDeferredValue(searchTerm);

// Use useTransition for interruptible updates
const [isPending, startTransition] = useTransition();
startTransition(() => {
  setSearchResults(results);
});

Next.js

// Use next/image for automatic optimization
import Image from 'next/image';

<Image
  src="/hero.jpg"
  width={1200}
  height={600}
  priority // Preloads for LCP
  alt="Hero"
/>

// Use next/font for font optimization
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });

Vue

// Async components
const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
);

// Use v-once for static content
<div v-once>{{ staticContent }}</div>

// Use v-memo for expensive renders
<div v-memo="[item.id]">...</div>

Monitoring and Continuous Improvement

Set Up Alerting

Track Core Web Vitals regression in CI/CD:

// Lighthouse CI configuration
module.exports = {
  ci: {
    assert: {
      assertions: {
        'largest-contentful-paint': ['warn', {maxNumericValue: 2500}],
        'cumulative-layout-shift': ['error', {maxNumericValue: 0.1}],
        'interactive': ['warn', {maxNumericValue: 3800}]
      }
    }
  }
};

A/B Testing Performance Changes

Measure impact of optimizations:

  • Run experiments with feature flags
  • Compare Core Web Vitals between variants
  • Use statistical significance testing

Common Issues and Solutions

ProblemSolution
Slow LCP from imagesPreload hero image, use modern formats
LCP blocked by fontsPreload fonts, use font-display: swap
High INP from third-party scriptsLoad asynchronously, use facade pattern
INP from heavy event handlersDebounce, use Web Workers
CLS from imagesAlways include width/height
CLS from adsReserve space with min-height
CLS from fontsUse size-adjust or local fallbacks

Tools Quick Reference

For debugging and testing your web performance, use these complementary tools:

Conclusion

Core Web Vitals are more than SEO checkboxes—they represent real user experience. By optimizing LCP, INP, and CLS, you create faster, more responsive, and more stable websites that users enjoy using.

Key takeaways:

  • LCP: Preload critical resources, optimize images, reduce server response time
  • INP: Break up long tasks, use Web Workers, debounce handlers
  • CLS: Reserve space for dynamic content, always specify image dimensions

For more developer resources, explore our free online tools. For detailed documentation, see web.dev Core Web Vitals and Chrome DevTools Performance docs.