Bỏ qua đến nội dung chính
Web VitalsperformanceCore Web VitalsSEOfrontend

Web Vitals: LCP, INP, CLS — Đo Và Tối Ưu Đầy Đủ

Web Vitals LCP INP CLS giải thích đầy đủ: cách đo, ngưỡng tốt, cách tối ưu thực tế. Code đo bằng web-vitals lib, gửi GA4. Ảnh hưởng SEO trực tiếp.

Xuất bản 9 phút đọc

Web Vitals không chỉ ảnh hưởng UX mà cả SEO ranking. Bài này đi qua LCP, INP, CLS chi tiết — cách đo, ngưỡng "Good", cách tối ưu thực tế từ kinh nghiệm production.

3 Core Web Vitals 2024

MetricĐo gìGoodCần cải thiệnKém
LCPLargest Contentful Paint≤ 2.5s2.5-4s> 4s
INPInteraction to Next Paint≤ 200ms200-500ms> 500ms
CLSCumulative Layout Shift≤ 0.10.1-0.25> 0.25

Google đánh giá p75 (75th percentile) — 75% user phải đạt "Good". Median không đủ.

LCP — Largest Contentful Paint

Đo thời gian element nội dung lớn nhất visible. Thường là hero image, video poster, h1 lớn.

Tối ưu LCP:

  • Preload hero image: <link rel="preload" as="image" href="/hero.webp">
  • Image format hiện đại: WebP, AVIF — nhỏ hơn JPEG 30-50%
  • Responsive image: srcset, sizes để mobile không tải image desktop
  • CDN edge: TTFB < 100ms (xem CDN là gì)
  • HTTP/2 + Brotli: tự động ở mọi CDN hiện đại
  • Critical CSS inline: render content nhanh, không chờ external CSS
  • Font display: optional: text render ngay với fallback
// Next.js: priority cho LCP image
import Image from 'next/image'

<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority           // ← preload
  sizes="(max-width: 768px) 100vw, 1200px"
/>

INP — Interaction to Next Paint

Đo độ trễ từ khi user tương tác (click, key, tap) đến khi browser paint frame mới phản hồi. Thay FID từ 3/2024.

Khác FID: FID chỉ đo INPUT đầu tiên. INP đo mọi interaction trong session, lấy cao nhất (98th percentile cho session dài).

Tối ưu INP:

// ❌ Long task block main thread
function expensiveCalculation(input) {
  let result = 0
  for (let i = 0; i < 1_000_000; i++) result += Math.sqrt(input * i)
  return result
}

button.onclick = () => {
  const result = expensiveCalculation(42)  // block 200ms
  setState(result)
}

// ✓ Defer với scheduler.yield hoặc setTimeout
button.onclick = async () => {
  await new Promise(r => setTimeout(r, 0))  // yield
  const result = expensiveCalculation(42)
  setState(result)
}

// ✓ Tốt hơn: Web Worker
const worker = new Worker('./compute.js')
button.onclick = () => {
  worker.postMessage({ input: 42 })
  worker.onmessage = (e) => setState(e.data)
}
  • Chia task > 50ms thành chunk nhỏ với scheduler.yield() (mới) hoặc setTimeout
  • Web Worker cho compute nặng
  • Reduce hydration cost (RSC giảm bundle)
  • Debounce input expensive operation
  • useTransition/startTransition cho non-urgent state

CLS — Cumulative Layout Shift

Đo tổng layout shift trong page lifecycle. Mỗi shift = impact_fraction × distance_fraction.

Nguyên nhân thường gặp:

  • Image không có width/height → load xong push content xuống
  • Ad banner load delay
  • Font swap (FOUT) → text size đổi
  • Lazy-loaded component không reserve space

Fix:

<!-- ❌ Không có dimension -->
<img src="/photo.jpg">

<!-- ✓ Có dimension → browser reserve space -->
<img src="/photo.jpg" width="800" height="600">

<!-- Hoặc CSS aspect-ratio -->
<img src="/photo.jpg" style="aspect-ratio: 4/3; width: 100%">
/* Font: dùng size-adjust + ascent-override để fallback giống webfont */
@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 22%;
}

body {
  font-family: 'Inter', 'Inter Fallback', sans-serif;
}
<!-- Reserve space cho ad/iframe -->
<div style="min-height: 250px">
  <ins class="adsbygoogle" />
</div>

Đo bằng web-vitals library

npm install web-vitals
// app/layout.tsx — đo và gửi GA4
'use client'
import { useEffect } from 'react'
import { onLCP, onINP, onCLS } from 'web-vitals'

export function VitalsTracker() {
  useEffect(() => {
    const send = (metric) => {
      if (typeof window.gtag !== 'function') return
      window.gtag('event', metric.name, {
        value: Math.round(metric.value),
        metric_id: metric.id,
        metric_value: metric.value,
        metric_delta: metric.delta,
      })
    }
    onLCP(send)
    onINP(send)
    onCLS(send)
  }, [])
  return null
}

GA4 sẽ có report Core Web Vitals theo page, device. Phân tích p75 cho từng URL — page nào kém biết để tối ưu.

RUM vs Lighthouse

  • Lighthouse: lab test 1 lần, máy tốt + throttle giả. Tốt cho dev test trước deploy.
  • CrUX (Chrome User Experience Report): RUM data từ user thật trên Chrome. Google dùng cho ranking.
  • web-vitals lib: anh tự đo từ user thật, gửi GA4/RUM tool.

Lighthouse score 100 không đảm bảo CrUX "Good" — tin RUM data, không tin lab.

Kết luận

Web Vitals giờ là nền tảng UX + SEO không thể bỏ qua. Tối ưu LCP, INP, CLS không khó về kỹ thuật — chỉ cần kỷ luật từ thiết kế đến code. Setup web-vitals lib đo từ user thật, dashboard GA4 theo dõi mỗi sprint. Tham khảo Image optimizationFont loading để khỏ kỷ luật mỗi metric.

Zalo