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ì | Good | Cần cải thiện | Kém |
|---|---|---|---|---|
| LCP | Largest Contentful Paint | ≤ 2.5s | 2.5-4s | > 4s |
| INP | Interaction to Next Paint | ≤ 200ms | 200-500ms | > 500ms |
| CLS | Cumulative Layout Shift | ≤ 0.1 | 0.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 optimization và Font loading để khỏ kỷ luật mỗi metric.