Core Web Vitals Visualizer

Interactive playground to trigger, measure, and understand LCP, INP & CLS in real-time

Real-Time Dashboard

Largest Contentful Paint
--
milliseconds
< 2500ms2500-4000ms> 4000ms
Waiting...
Interaction to Next Paint
--
milliseconds
< 200ms200-500ms> 500ms
Waiting...
Cumulative Layout Shift
--
score (unitless)
< 0.10.1-0.25> 0.25
Waiting...

Event Log

--:--:--SYSTEMObservers initialized. Interact with the page to generate metrics.

LCP — Largest Contentful Paint

LCP measures when the largest content element becomes visible. It re-fires as larger elements render. Try adding images or text blocks and watch the LCP metric update.

Trigger LCP Changes

LCP Demo Area

INP — Interaction to Next Paint

INP measures the worst interaction latency. It captures the delay from user input to the next paint. Click buttons below that run expensive synchronous JavaScript to see INP increase.

Trigger Interactions

INP Demo Area

CLS — Cumulative Layout Shift

CLS measures unexpected layout shifts. Elements injected without reserved space cause shifts. Try injecting elements to see CLS accumulate.

Trigger Layout Shifts

CLS Demo Area
This is existing content. Watch what happens when elements are injected above without reserved space.

Metric Timeline

Visual timeline showing when each metric event fires relative to page load.

Events Over Time

Interact with the page to see events appear here
LCP INP CLS

Before / After Comparison

Side-by-side comparison of unoptimized vs optimized patterns for each Core Web Vital.

LCP: Image Loading

✗ Unoptimized

Large image without dimensions, no priority hints. Browser discovers late, layout shifts during load.

<img src="hero.jpg">

✓ Optimized

Explicit dimensions, fetchpriority="high", preload link. Browser starts loading immediately.

<link rel="preload" as="image"
  href="hero.webp">
<img src="hero.webp"
  width="800" height="400"
  fetchpriority="high"
  decoding="async">

INP: Event Handlers

✗ Unoptimized

Heavy synchronous work in click handler blocks the main thread and delays paint.

button.onclick = () => {
  // 500ms of synchronous work
  const data = heavyComputation();
  updateDOM(data);
};

✓ Optimized

Show immediate feedback, defer heavy work with requestIdleCallback or setTimeout.

button.onclick = () => {
  button.textContent = 'Processing...';
  // Yield to browser for paint
  setTimeout(() => {
    const data = heavyComputation();
    updateDOM(data);
  }, 0);
};

CLS: Dynamic Content

✗ Unoptimized

Ad or banner injected with no reserved space. Pushes content down unexpectedly.

<!-- Content loads, then ad injects above -->
<div id="ad"></div>
<main>Article content...</main>

<script>
  // Ad loads later, 250px tall
  document.getElementById('ad')
    .innerHTML = '<img src="ad.jpg">';
</script>

✓ Optimized

Reserve space with min-height or aspect-ratio. Content stays in place.

<div id="ad" style="
  min-height: 250px;
  contain: layout;
"></div>
<main>Article content...</main>

<script>
  // Ad fills reserved space, no shift
  document.getElementById('ad')
    .innerHTML = '<img src="ad.jpg"
      width="728" height="250">';
</script>

Preset Scenarios

Click a scenario to automatically trigger a series of actions and see how metrics respond.

✅ Perfect Score

Minimal page with small, pre-sized content. All metrics stay green.

LCPINPCLS

🖼 Heavy Images

Load multiple large images without optimization. LCP degrades.

LCP

⏳ Blocking JavaScript

Click handlers with expensive sync operations. INP spikes.

INP

📢 Ad Injection Storm

Rapidly inject elements without reserved space. CLS accumulates.

CLS

🐢 Slow Page Load

Simulates a poorly optimized page: large LCP, layout shifts, heavy interactions.

LCPINPCLS

🔄 SPA Navigation

Simulate a single-page app route change with content swap and late-loading elements.

LCPCLS

🔤 Font Swap Flash

Simulates FOUT (Flash of Unstyled Text) causing layout shift when fonts load.

CLSLCP

📝 Interactive Form

Form with validation that blocks main thread. Tests INP during user input.

INP

Measurement Code

Copy these snippets to measure Core Web Vitals on your own site.

// Measure LCP with PerformanceObserver
new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const last = entries[entries.length - 1];
  console.log('LCP:', last.startTime, 'ms');
  console.log('Element:', last.element);
  console.log('Size:', last.size);
}).observe({ type: 'largest-contentful-paint', buffered: true });

// Thresholds (Google):
// Good:  < 2500ms
// Needs Improvement: 2500-4000ms
// Poor:  > 4000ms
// Measure INP with PerformanceObserver
// INP = worst interaction latency (p98 for high-traffic)
let worstINP = 0;
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // duration includes input delay + processing + presentation
    if (entry.duration > worstINP) {
      worstINP = entry.duration;
      console.log('INP candidate:', entry.duration, 'ms');
      console.log('Target:', entry.target);
      console.log('Name:', entry.name); // e.g., "click", "keydown"
    }
  }
}).observe({ type: 'event', buffered: true, durationThreshold: 16 });

// Thresholds (Google):
// Good:  < 200ms
// Needs Improvement: 200-500ms
// Poor:  > 500ms
// Measure CLS with PerformanceObserver
let clsScore = 0;
let sessionEntries = [];
let sessionValue = 0;
let maxSessionValue = 0;

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // Only count shifts without recent user input
    if (!entry.hadRecentInput) {
      // Session window: gap < 1s, total < 5s
      const last = sessionEntries[sessionEntries.length - 1];
      if (last && entry.startTime - last.startTime < 1000
          && entry.startTime - sessionEntries[0].startTime < 5000) {
        sessionValue += entry.value;
      } else {
        sessionEntries = [];
        sessionValue = entry.value;
      }
      sessionEntries.push(entry);
      maxSessionValue = Math.max(maxSessionValue, sessionValue);
      console.log('CLS:', maxSessionValue.toFixed(4));
    }
  }
}).observe({ type: 'layout-shift', buffered: true });

// Thresholds (Google):
// Good:  < 0.1
// Needs Improvement: 0.1-0.25
// Poor:  > 0.25
// All Core Web Vitals in one snippet
// Production-ready: use the web-vitals library instead
// npm install web-vitals

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

onLCP((metric) => {
  console.log('LCP:', metric.value, metric.rating);
  // metric.rating = 'good' | 'needs-improvement' | 'poor'
  sendToAnalytics({ name: 'LCP', ...metric });
});

onINP((metric) => {
  console.log('INP:', metric.value, metric.rating);
  sendToAnalytics({ name: 'INP', ...metric });
});

onCLS((metric) => {
  console.log('CLS:', metric.value, metric.rating);
  sendToAnalytics({ name: 'CLS', ...metric });
});

function sendToAnalytics(data) {
  // navigator.sendBeacon for reliability
  navigator.sendBeacon('/analytics', JSON.stringify(data));
}

Browser Support

Core Web Vitals rely on specific Performance APIs. Support varies by browser.

This Browser's Capabilities

General Browser Support

🌐
Chrome
Full Support
🌐
Edge
Full Support
🔥
Firefox
Partial (no LCP/INP)
🍎
Safari
Partial (no LCP/INP)
📱
Chrome Android
Full Support
📱
Safari iOS
Partial (CLS only)