正在加载,请稍候…

Core Web Vitals 2026: INP, LCP, CLS Optimization and Real-User Monitoring

Optimize for Core Web Vitals. INP (Interaction to Next Paint) optimization with scheduler API, LCP with preloading, CLS prevention, and real-user monitoring setup.

Google replaced FID with INP in March 2024. INP measures the worst interaction latency during a page visit — not just the first one.

Core Web Vitals 2026

Metric What It Measures Good Poor
LCP Largest content paint time ≤2.5s >4s
INP Worst interaction response ≤200ms >500ms
CLS Cumulative layout shift ≤0.1 >0.25

Measuring with web-vitals Library

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

function sendToAnalytics({ name, value, id, rating, navigationType }) {
  fetch('/api/vitals', {
    method: 'POST',
    body: JSON.stringify({
      metric: name,
      value: Math.round(name === 'CLS' ? value * 1000 : value),
      id, rating, navigationType,
      url: window.location.href,
    }),
    keepalive: true
  })
}

onLCP(sendToAnalytics, { reportAllChanges: true })
onINP(sendToAnalytics, { reportAllChanges: true })
onCLS(sendToAnalytics, { reportAllChanges: true })

Optimizing LCP

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

<!-- LCP image: never lazy-load, always include dimensions -->
<img
  src="/hero.webp"
  width="1200" height="600"
  alt="Hero"
  fetchpriority="high"
>
<!-- fetchpriority="high" tells browser to load this ASAP -->
<!-- width/height prevent layout shift while loading -->

Optimizing INP: Break Long Tasks

INP problems come from synchronous code blocking the main thread during interactions.

// Bad: 500ms synchronous work during click
button.addEventListener('click', () => {
  const result = expensiveComputation(largeData)  // Blocks paint
  updateUI(result)
})

// Good: yield between chunks
button.addEventListener('click', async () => {
  button.disabled = true  // Immediate visual feedback

  const results = []
  for (let i = 0; i < largeData.length; i += 100) {
    results.push(...processChunk(largeData.slice(i, i + 100)))
    // Yield to allow browser to paint and handle other inputs
    await scheduler.yield()
    // Fallback: await new Promise(resolve => setTimeout(resolve, 0))
  }

  updateUI(results)
  button.disabled = false
})

// Heavy computation → Web Worker
const worker = new Worker('/workers/compute.js')
button.addEventListener('click', () => {
  worker.postMessage({ data: largeData })
  worker.onmessage = ({ data: result }) => updateUI(result)
})

Preventing CLS

/* Reserve space for images/videos */
.hero-image {
  aspect-ratio: 16 / 9;
  width: 100%;
  object-fit: cover;
  /* Browser knows height before image loads */
}

/* Reserve space for ads */
.ad-slot {
  min-height: 250px;
  width: 300px;
}

/* Use skeleton screens instead of showing content after load */
.skeleton-text {
  height: 1em;
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: loading 1.5s infinite;
}

Real-User Monitoring at P75

// Google measures P75 (75th percentile), not average
function calculateP75(values) {
  const sorted = [...values].sort((a, b) => a - b)
  return sorted[Math.floor(sorted.length * 0.75)]
}

function analyzeVitals(data) {
  const byPage = {}
  for (const entry of data) {
    if (!byPage[entry.url]) byPage[entry.url] = []
    byPage[entry.url].push(entry.lcp_value)
  }
  return Object.entries(byPage).map(([url, values]) => ({
    url, p75_lcp: calculateP75(values), count: values.length
  }))
}

Core Web Vitals are ranking factors AND user experience signals. Investing in them pays dividends in both SEO and conversion rate.

→ Analyze your page timing data with the Unix Timestamp tool.