Năm 2024, Google chính thức đưa Interaction to Next Paint (INP) thay thế First Input Delay (FID) trở thành một trong ba Core Web Vitals chính thức. Cùng với Largest Contentful Paint (LCP) và Cumulative Layout Shift (CLS), ba chỉ số này chi phối trực tiếp trải nghiệm người dùng và thứ hạng tìm kiếm của website.
Trong bài viết này, tôi sẽ chia sẻ cách tôi đã tối ưu những chỉ số này từ dự án thực tế — một website thương mại điện tử cho chuỗi phòng khám thẩm mỹ với 15,000 người dùng hàng tháng. Trước khi tối ưu, điểm Lighthouse chỉ đạt 62. Sau ba tuần, trang đạt 99 điểm trên cả mobile và desktop.
Core Web Vitals là gì và tại sao chúng quan trọng
Core Web Vitals là bộ chỉ số do Google định nghĩa để đo lường trải nghiệm người dùng trên web. Ba chỉ số chính:
Largest Contentful Paint (LCP) — Thời gian để nội dung lớn nhất hiển thị trên màn hình. Mục tiêu: dưới 2.5 giây. LCP thường bị ảnh hưởng bởi ảnh không optimized, render-blocking JavaScript, hoặc server response chậm.
Interaction to Next Paint (INP) — Thời gian từ khi người dùng tương tác đến khi trình duyệt phản hồi. Mục tiêu: dưới 200ms. INP thay thế FID từ tháng 3/2024 và đo lường toàn bộ interactions trong suốt session, không chỉ lần đầu.
Cumulative Layout Shift (CLS) — Mức độ layout di chuyển không mong muốn. Mục tiêu: dưới 0.1. Nguyên nhân thường gặp: ảnh không có kích thước, quảng cáo động, web fonts gây FOUT.
Đo lường hiện trạng: CrUX vs Lighthouse
Trước khi tối ưu, tôi luôn đo lường hai nguồn dữ liệu khác nhau:
Lighthouse chạy trong môi trường lab kiểm soát — kết nối 4G, CPU 4x slowdown. Tốt để phát hiện vấn đề và so sánh trước/sau trong cùng điều kiện.
PageSpeed Insights cung cấp cả dữ liệu lab (Lighthouse) và dữ liệu thực tế (CrUX) từ người dùng Chrome thật trong 28 ngày gần nhất.
Chrome DevTools Performance panel là công cụ mạnh nhất để phân tích sâu — xem flame chart, network waterfall, và identify main-thread bottlenecks.
// Measure INP with web-vitals library
import { onINP } from 'web-vitals';
onINP(({ value, attribution }) => {
console.log('INP:', value);
console.log('Interaction:', attribution.eventType);
console.log('Processing time:', attribution.eventDuration, 'ms');
console.log('Presentation delay:', attribution.processingStart - attribution.eventEnd, 'ms');
// Send to analytics
sendToAnalytics({
name: 'INP',
value,
rating: value < 200 ? 'good' : value < 500 ? 'needs-improvement' : 'poor',
});
});Tối ưu LCP: Từ 4.2s xuống 1.8s
LCP của website thẩm mỹ ban đầu là 4.2s trên mobile — rất kém. Nguyên nhân chính:
Ảnh hero không optimized. Trang dùng ảnh PNG 2.4MB làm hero. Tôi đã chuyển sang WebP với responsive images:
Kết quả: LCP giảm từ 4.2s xuống 1.8s chỉ với việc tối ưu ảnh. Đây là impact lớn nhất với effort thấp nhất.
// Next.js Image với priority + sizes
import Image from 'next/image';
export function HeroImage() {
return (
<Image
src="/hero-clinic.webp"
alt="NexZen Clinic — dịch vụ thẩm mỹ nội bất"
fill
priority // QUAN TRỌNG: preload ảnh LCP
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
quality={85}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
className="object-cover"
/>
);
}Facing similar challenges?
I can help you optimize your website — reach out for a free consultation.
Tối ưu INP: Giải phóng main thread
INP cao nhất trên trang là 890ms — rất kém. Nguyên nhân chính là một third-party chat widget chạy heavy JavaScript trên main thread ngay khi page load.
Chiến lược tối ưu INP:
1. Defer non-critical JS. Tải chat widget sau khi page đã interactive bằng dynamic import:
2. Break up long tasks. Nếu có logic nặng trên main thread, chia nhỏ bằng scheduler.yield() hoặc requestIdleCallback:
3. Optimize event handlers. Tránh layout thrashing trong handlers bằng cách đọc layout values trước, rồi mới write DOM changes.
// Dynamic import để tách chunk
const ChatWidget = dynamic(() => import('./ChatWidget'), {
loading: () => null,
ssr: false,
});
// Trong component:
const handleClick = async (e) => {
// UI feedback ngay lập tức
setLoading(true);
// Heavy computation: yield cho main thread breathe
await scheduler.yield();
// Logic nặng
const result = await heavyComputation(data);
setLoading(false);
setResult(result);
};Tối ưu CLS: Ngăn layout shift
CLS ban đầu là 0.24 — cao hơn ngưỡng "poor" của Google. Nguyên nhân chính:
Web fonts gây FOUT (Flash of Unstyled Text). Khi web font chưa load, trình duyệt dùng fallback font có metrics khác → text reflow khi font load.
Giải pháp:
1. Dùng font-display: optional cho body text — trình duyệt bỏ qua font nếu network chậm
2. Preload font files quan trọng nhất
3. Sử dụng @font-face size-adjust và ascent-override để match fallback font metrics
4. Đặt explicit width/height trên ảnh và aspect-ratio containers
/* Preload critical font */
<link rel="preload" href="/fonts/geist.woff2" as="font" type="font/woff2" crossorigin>
/* CSS font-face với size-adjust */
@font-face {
font-family: 'GeistFallback';
src: local('Arial');
size-adjust: 107%;
ascent-override: 95%;
descent-override: 25%;
}
body {
font-family: 'Geist', 'GeistFallback', system-ui, sans-serif;
font-display: optional; /* Không block render, không reflow */
}Kết quả sau ba tuần tối ưu
Sau khi áp dụng tất cả optimizations trên: