Tối ưu Core Web Vitals 2026: Giảm LCP, INP, CLS cho website thực chiến

2 tháng trước · 10 phút đọc
Tại sao Core Web Vitals quan trọng hơn bao giờ hết
Mình từng bỏ qua phần "Performance" trong Lighthouse suốt 2 năm. Điểm 45/100 nhưng app vẫn chạy được, khách hàng không phàn nàn nhiều. Cho đến khi SEO ranking tụt 3 trang và bounce rate nhảy lên 68% - lúc đó mình mới ngồi đọc lại docs của Google.
Core Web Vitals không chỉ là điểm số đẹp để show với sếp. Từ năm 2021, Google dùng nó như một ranking factor trực tiếp. Năm 2024, INP (Interaction to Next Paint) chính thức thay thế FID, và năm 2026 trọng số của cả ba chỉ số đang được nâng lên trong thuật toán tìm kiếm.
Ba chỉ số cần quan tâm:
- LCP (Largest Contentful Paint): Thời gian render phần tử lớn nhất - ảnh hero, heading chính. Mục tiêu: dưới 2.5s
- INP (Interaction to Next Paint): Thời gian phản hồi khi người dùng click/tap/type. Mục tiêu: dưới 200ms
- CLS (Cumulative Layout Shift): Mức độ giật layout khi tải trang. Mục tiêu: dưới 0.1
Ba chỉ số - ba vấn đề khác nhau, cần fix theo từng hướng riêng
Google có một thống kê khá thú vị: Bounce probability rises 32% as load time increases from 1 second to 3 seconds (Google research and industry statistics). Con số này đủ để justify việc bỏ thời gian tối ưu.
Trước khi đi vào fix cụ thể, bước quan trọng nhất là đo đúng. Dùng PageSpeed Insights để lấy lab data (môi trường kiểm soát) và Search Console để xem field data (dữ liệu thực từ người dùng thật). Hai nguồn này thường khác nhau đáng kể - field data mới là thứ Google dùng để rank.
Fix LCP: Làm cho trang tải nhanh hơn một cách nhìn thấy được
LCP chậm thường đến từ 3 nguồn chính: ảnh chưa tối ưu, server response chậm, và render-blocking resources. Mình sẽ đi theo thứ tự impact từ cao xuống thấp.
Tối ưu ảnh - fix có impact lớn nhất
Đây thường là thủ phạm số 1. Ảnh hero 2MB JPEG load trên 3G? LCP của bạn sẽ tệ ngay.
Hai điểm quan trọng hay bị bỏ sót:
fetchpriority="high"trên LCP element: báo cho browser biết fetch ảnh này trước. Không có attribute này, browser có thể defer nó.widthvàheightcụ thể: giúp browser tính trước layout space, giảm CLS đồng thời.
Thiếu fetchpriority trên ảnh hero là lý do LCP cao hơn 0.5-1s so với cần thiết
Preload LCP resource
Nếu LCP element là ảnh background CSS hoặc font, browser không biết trước để fetch sớm:
Server response time (TTFB)
Nếu server trả về HTML chậm hơn 600ms, LCP gần như chắc chắn fail. Một vài fix phổ biến:
Với Next.js hoặc các SSR framework, đảm bảo bạn đang dùng ISR (Incremental Static Regeneration) thay vì SSR thuần cho các trang không thay đổi thường xuyên. TTFB của ISR thường dưới 100ms so với SSR có thể lên 500ms+.
Fix INP: Làm cho trang phản hồi click trong 200ms
INP là chỉ số khó fix nhất vì nó đo cái mà người dùng cảm nhận trực tiếp khi tương tác. Click một button mà phải đợi 500ms mới thấy phản hồi? INP của bạn đang fail.
Vấn đề thường đến từ Long Tasks - những đoạn JavaScript chạy trên main thread hơn 50ms liên tục, block mọi tương tác trong thời gian đó.
Tìm Long Tasks với Chrome DevTools
Mở DevTools → Performance tab → Record khi interact với trang. Những task màu đỏ/vàng dài hơn 50ms là thủ phạm.
Long Tasks màu đỏ trong Performance tab - đây là nơi bắt đầu debug INP
Tách nhỏ Long Tasks
Thay vì chạy một đoạn code lớn một lần, chia nhỏ và yield về cho main thread:
Debounce input handlers
Event handlers chạy trên mỗi keystroke mà không có debounce là nguyên nhân phổ biến với search box và form validation:
Với React, đừng để heavy computation trong render cycle. Dùng useMemo và useCallback đúng chỗ - không phải wrap tất cả, mà wrap những gì thật sự expensive. Nếu muốn hiểu sâu hơn về JavaScript performance và cách browser xử lý event loop, khóa Lập Trình JavaScript Nâng Cao có section riêng về async và performance khá chi tiết.
Fix CLS: Chấm dứt layout nhảy lung tung
CLS tệ thường xảy ra đột ngột và khó reproduce - người dùng đang đọc thì layout shift, click nhầm vào quảng cáo thay vì button. Khó chịu, và Google phạt nặng.
Ba nguyên nhân phổ biến nhất:
1. Ảnh không có kích thước
Browser không biết ảnh to bao nhiêu trước khi tải xong, nên không reserve space:
2. Font chữ thay thế (FOUT)
Browser load fallback font trước, sau đó swap sang custom font làm text nhảy vị trí:
font-display: optional loại bỏ layout shift hoàn toàn, trade-off là font tải chậm có thể không hiển thị
3. Nội dung inject động (ads, banners)
Với các component lazy load trong React, dùng Suspense với fallback có cùng kích thước:
Đo đúng với Lighthouse và PageSpeed Insights
Mình thấy nhiều bạn chỉ chạy Lighthouse một lần rồi cố fix để điểm lên 90+. Thực ra không phải thế.
Lab data vs Field data - đây là điểm quan trọng nhất cần hiểu.
Lighthouse (lab data) đo trong môi trường giả lập: CPU throttle 4x, mạng 3G Fast. Hữu ích để debug nhưng không phản ánh trải nghiệm người dùng thực. Field data từ Chrome UX Report (CrUX) mới là thứ Google dùng để rank - và nó lấy từ người dùng Chrome thật, với device và mạng thật của họ.
Lab data vs Field data thường chênh nhau 30-50% - đừng chỉ nhìn điểm Lighthouse
Thêm monitoring vào production
Dùng web-vitals library để lấy field data ngay từ codebase:
Package web-vitals nhỏ gọn (~1.5KB), không ảnh hưởng performance. Bạn có thể gửi data về Google Analytics 4 hoặc bất kỳ endpoint nào. Điểm mạnh là nó đo real user experience thay vì lab environment.
Case study thực tế: Website thương mại điện tử Việt
Mình sẽ chia sẻ một case mình tham gia tối ưu - website bán hàng với ~50k pageviews/tháng, stack Next.js + Vercel.
Baseline trước khi optimize (field data từ Search Console):
- LCP: 4.8s (Kém)
- INP: 380ms (Kém)
- CLS: 0.28 (Kém)
Cả ba đều đỏ. Điểm Lighthouse mobile: 38/100.
Từ điểm 38 lên 84 - không magic, chỉ là làm đúng thứ tự ưu tiên
Những gì đã làm (theo thứ tự impact):
1. Fix LCP - 2 ngày
Ảnh hero 1.8MB JPEG → chuyển sang AVIF với srcset. Thêm fetchpriority="high" và <link rel="preload">. LCP giảm từ 4.8s → 2.1s.
2. Fix CLS - nửa ngày
Thêm width và height cho tất cả <img>. Đổi banner ads từ inject sau render sang pre-reserved container. CLS giảm từ 0.28 → 0.04.
3. Fix INP - 3 ngày (phức tạp nhất)
Phát hiện một component filter sản phẩm có Long Task 450ms mỗi lần click. Nguyên nhân: re-render toàn bộ 200 product cards. Fix bằng React.memo + virtualization với react-window. INP giảm từ 380ms → 145ms.
Kết quả sau 6 tuần (đợi Google crawl lại và cập nhật CrUX):
- LCP: 2.1s → Tốt
- INP: 145ms → Tốt
- CLS: 0.04 → Tốt
- Lighthouse mobile: 38 → 84
- Organic traffic: tăng 31% organic search traffic growth over 90 days after optimizing Core Web Vitals to 'Good' (Shopify Plus e-commerce case study) (Shopify Plus E-commerce Case Study) so với 3 tháng trước
Điều mình học được: thứ tự ưu tiên quan trọng hơn làm tất cả cùng lúc. LCP ảnh hưởng ngay đến cảm nhận đầu tiên, fix trước. CLS nhanh fix hơn INP nhiều, làm tiếp. INP để cuối vì cần profiling kỹ.
Tải checklist đầy đủ ở đây: Tải checklist Core Web Vitals 2026
