正在加载,请稍候…

Web Performance Optimization: Core Web Vitals Guide for 2026

Improve your Core Web Vitals scores (LCP, INP, CLS) with practical optimization techniques. Learn image optimization, code splitting, lazy loading, and how Google measures performance.

Web Performance Optimization: Core Web Vitals Guide for 2026

Core Web Vitals directly impact Google search rankings. In 2026, Google uses three metrics to measure user experience: LCP, INP, and CLS. Here's how to improve each one.

The Three Core Web Vitals

LCP (Largest Contentful Paint)

Measures: How quickly the largest visible element loads Good: < 2.5s | **Needs Improvement**: 2.5s–4s | **Poor**: > 4s

The LCP element is usually:

  • A hero image
  • A large heading
  • A background image

INP (Interaction to Next Paint)

Measures: Responsiveness to user interactions (replaced FID in 2024) Good: < 200ms | **Needs Improvement**: 200ms–500ms | **Poor**: > 500ms

CLS (Cumulative Layout Shift)

Measures: Visual stability — how much content unexpectedly moves Good: < 0.1 | **Needs Improvement**: 0.1–0.25 | **Poor**: > 0.25

Diagnosing Issues

// Measure Core Web Vitals in your app
import { getLCP, getINP, getCLS } from 'web-vitals';

getLCP(metric => {
  console.log('LCP:', metric.value, metric.rating);
  // Send to analytics
  analytics.track('web_vital', { name: 'LCP', value: metric.value, rating: metric.rating });
});

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

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

Improving LCP

1. Optimize the LCP Image

<!-- ❌ No priority hint — browser doesn't know this is important -->
<img src="/hero.jpg" alt="Hero" />

<!-- ✅ fetchpriority="high" tells browser to load this first -->
<img 
  src="/hero.jpg" 
  alt="Hero"
  fetchpriority="high"
  loading="eager"
  decoding="async"
/>

2. Use Modern Image Formats

<!-- ✅ Serve WebP/AVIF with fallback -->
<picture>
  <source type="image/avif" srcset="/hero.avif" />
  <source type="image/webp" srcset="/hero.webp" />
  <img src="/hero.jpg" alt="Hero" fetchpriority="high" />
</picture>
# Convert images with sharp (Node.js)
const sharp = require('sharp');

await sharp('hero.jpg')
  .resize(1200)
  .webp({ quality: 80 })
  .toFile('hero.webp');

await sharp('hero.jpg')
  .resize(1200)
  .avif({ quality: 65 })
  .toFile('hero.avif');

3. Eliminate Render-Blocking Resources

<!-- ❌ Render-blocking CSS -->
<link rel="stylesheet" href="/styles.css" />

<!-- ✅ Preload critical CSS -->
<link rel="preload" href="/critical.css" as="style" onload="this.rel='stylesheet'" />

<!-- ✅ Inline critical CSS -->
<style>
  /* Only above-the-fold styles */
  body { margin: 0; font-family: sans-serif; }
  .hero { min-height: 100vh; }
</style>
<link rel="stylesheet" href="/styles.css" media="print" onload="this.media='all'" />
<!-- ❌ Render-blocking JS -->
<script src="/app.js"></script>

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

4. Preconnect to Critical Origins

<!-- Tell browser to start DNS + TCP + TLS early -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="dns-prefetch" href="https://api.example.com" />

5. Use a CDN

Without CDN (user in Tokyo, server in New York):
  DNS resolution: ~150ms
  TCP handshake: ~180ms  
  TLS: ~180ms
  TTFB: ~400ms
  Total: ~910ms before any content!

With CDN (edge in Tokyo):
  DNS: ~5ms
  TCP: ~10ms
  TLS: ~10ms
  TTFB: ~50ms
  Total: ~75ms ← 12x faster

Improving INP

INP measures the time from user interaction to visual feedback. Poor INP = laggy, unresponsive UI.

1. Break Up Long Tasks

// ❌ Long task — blocks the main thread
function processAllItems(items) {
  // This runs for 2 seconds, blocking all input
  items.forEach(item => heavyProcess(item));
}

// ✅ Yield to the browser between chunks
async function processAllItemsAsync(items) {
  const CHUNK_SIZE = 50;
  
  for (let i = 0; i < items.length; i += CHUNK_SIZE) {
    const chunk = items.slice(i, i + CHUNK_SIZE);
    chunk.forEach(item => heavyProcess(item));
    
    // Yield to browser — allows input events to be processed
    await new Promise(resolve => setTimeout(resolve, 0));
    // Or use scheduler.yield() in browsers that support it:
    // await scheduler.yield();
  }
}

2. Use Web Workers for Heavy Computation

// Move heavy work off the main thread
const worker = new Worker(new URL('./heavy-worker.js', import.meta.url));

button.addEventListener('click', () => {
  // Main thread stays responsive
  worker.postMessage({ data: largeDataset });
  worker.onmessage = ({ data }) => {
    updateUI(data.result); // Update UI when worker is done
  };
});

3. Debounce Expensive Event Handlers

// ❌ Runs on every keystroke
input.addEventListener('input', () => {
  expensiveSearch(input.value); // Freezes UI
});

// ✅ Debounce — waits 300ms after user stops typing
import { debounce } from 'lodash-es';

const handleSearch = debounce((value) => {
  expensiveSearch(value);
}, 300);

input.addEventListener('input', () => handleSearch(input.value));

4. React Concurrent Features

import { useTransition, startTransition } from 'react';

function SearchResults() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  function handleSearch(e) {
    const value = e.target.value;
    setQuery(value); // Urgent — update input immediately
    
    startTransition(() => {
      // Non-urgent — can be interrupted by more urgent updates
      setResults(heavyFilterOperation(value));
    });
  }

  return (
    <div>
      <input value={query} onChange={handleSearch} />
      {isPending ? <Spinner /> : <ResultsList results={results} />}
    </div>
  );
}

Improving CLS

CLS is caused by elements shifting after the page loads — images without dimensions, dynamically injected content, fonts swapping.

1. Always Specify Image Dimensions

<!-- ❌ No dimensions — browser doesn't reserve space -->
<img src="/photo.jpg" alt="Photo" />

<!-- ✅ Explicit dimensions -->
<img src="/photo.jpg" alt="Photo" width="800" height="600" />

<!-- ✅ Or use aspect-ratio in CSS -->
<img src="/photo.jpg" alt="Photo" style="aspect-ratio: 4/3; width: 100%;" />

2. Reserve Space for Dynamic Content

/* Reserve space for ads/embeds before they load */
.ad-slot {
  min-height: 250px;
  background: #f5f5f5;
}

/* Reserve space for async content */
.user-avatar-skeleton {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: #e5e7eb;
  animation: pulse 1.5s ease-in-out infinite;
}

3. Font Display Optimization

/* ❌ FOUT (Flash of Unstyled Text) causes layout shift */
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/myfont.woff2');
  /* font-display defaults to 'auto' */
}

/* ✅ Use font-display: optional or swap with size-adjust */
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/myfont.woff2');
  font-display: swap;
}

/* ✅ Best: Use size-adjust to match system font metrics */
@font-face {
  font-family: 'MyFont Fallback';
  src: local('Arial');
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
  size-adjust: 107%;
}

Code Splitting

// ✅ React lazy loading — split at route level
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics'));

function App() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/analytics" element={<Analytics />} />
      </Routes>
    </Suspense>
  );
}

Performance Monitoring

// Report to analytics
import { getLCP, getINP, getCLS, getFID, getFCP, getTTFB } from 'web-vitals';

function sendToAnalytics({ name, value, rating, id }) {
  // Google Analytics 4
  gtag('event', name, {
    value: Math.round(name === 'CLS' ? value * 1000 : value),
    metric_id: id,
    metric_rating: rating,
  });
}

getLCP(sendToAnalytics);
getINP(sendToAnalytics);
getCLS(sendToAnalytics);
getFCP(sendToAnalytics);
getTTFB(sendToAnalytics);

Quick Wins Checklist

  • Enable gzip/brotli compression on your server
  • Add fetchpriority="high" to LCP image
  • Set explicit width and height on all images
  • Use WebP/AVIF image formats
  • Lazy load below-the-fold images: loading="lazy"
  • Add defer to non-critical scripts
  • Preconnect to critical third-party origins
  • Implement route-based code splitting
  • Set font-display: swap for web fonts
  • Use a CDN for static assets

→ Benchmark your page performance metrics with the Benchmark Builder.