# ✅ Checklist Tối Ưu Core Web Vitals 2026
> **Mục tiêu:** LCP < 2.5s · CLS < 0.1 · INP < 200ms
> Cập nhật theo tiêu chuẩn Google Search 2026 — kiểm tra định kỳ mỗi sprint hoặc trước mỗi lần release.
---
## 📋 Hướng Dẫn Sử Dụng
- ☐ = Chưa thực hiện
- ✅ = Đã hoàn thành
- ⚠️ = Cần xem xét lại
- Ưu tiên thực hiện theo thứ tự từ trên xuống trong mỗi phần
- Gắn nhãn **[CRITICAL]** = ảnh hưởng trực tiếp đến ranking Google
---
## PHẦN 1 — 🖼️ LCP Optimization (Largest Contentful Paint)
> **Ngưỡng đạt:** LCP ≤ 2.5 giây (đo tại P75 của field data)
> LCP là yếu tố xếp hạng quan trọng nhất trong Core Web Vitals. Đây thường là ảnh hero, banner đầu trang hoặc khối text lớn nhất.
---
- [ ] **[CRITICAL] Xác định đúng phần tử LCP trên từng loại trang**
> *Tại sao:* Mỗi template trang (homepage, product, blog) có LCP element khác nhau. Dùng DevTools → Performance tab hoặc WebPageTest để xác định chính xác. Tối ưu sai phần tử = lãng phí toàn bộ công sức.
- [ ] **[CRITICAL] Thêm `fetchpriority="high"` vào thẻ ` ` của LCP image**
> *Tại sao:* Thuộc tính này yêu cầu trình duyệt tải ảnh LCP ngay lập tức với băng thông cao nhất, bỏ qua hàng chờ mặc định. Có thể giảm LCP từ 0.3–0.8 giây chỉ với một dòng code.
```html
```
- [ ] **[CRITICAL] Thêm ` ` cho LCP image trong `
`**
> *Tại sao:* Browser discovery thông thường phải đợi HTML parse xong mới biết cần tải ảnh. Preload giúp trình duyệt khởi động tải ảnh ngay từ byte HTML đầu tiên, giảm đáng kể thời gian chờ.
```html
```
- [ ] **Loại bỏ `loading="lazy"` khỏi ảnh LCP**
> *Tại sao:* `loading="lazy"` trì hoãn tải ảnh cho đến khi người dùng cuộn đến gần. Áp dụng nhầm cho ảnh LCP (thường nằm above-the-fold) là nguyên nhân phổ biến nhất khiến LCP thất bại — chỉ dùng lazy cho ảnh below-the-fold.
- [ ] **Đảm bảo server trả về LCP resource trong vòng 600ms (TTFB ≤ 800ms)**
> *Tại sao:* Google phân tách LCP thành 4 giai đoạn; TTFB cao ảnh hưởng toàn bộ pipeline. Nếu server chậm, mọi tối ưu frontend đều vô nghĩa. Dùng caching, CDN edge, và server-side rendering để cải thiện.
- [ ] **Inline critical CSS liên quan đến LCP element vào ``**
> *Tại sao:* Nếu CSS của phần tử LCP nằm trong file external stylesheet, trình duyệt phải chờ download + parse CSS mới render được. Inline CSS block rendering (~200–400 bytes cho LCP element) loại bỏ round-trip này.
- [ ] **Giảm chuỗi redirect trước khi tải LCP resource về 0 hoặc tối đa 1**
> *Tại sao:* Mỗi redirect thêm 100–300ms latency do phải thiết lập connection mới. Chuỗi redirect `http → https → www → non-www` có thể ngốn 600ms+ trước khi tải bất cứ thứ gì.
- [ ] **Sử dụng `rel="preconnect"` cho các origin chứa LCP resource (CDN, font server)**
> *Tại sao:* Kết nối đến third-party domain (DNS lookup + TCP + TLS) tốn 150–300ms. Preconnect thực hiện handshake này sớm khi trình duyệt còn đang parse HTML, tiết kiệm toàn bộ thời gian đó khi resource thực sự cần tải.
```html
```
---
## PHẦN 2 — 📐 CLS Prevention (Cumulative Layout Shift)
> **Ngưỡng đạt:** CLS ≤ 0.1 (tổng điểm dịch chuyển layout trong suốt vòng đời trang)
> CLS ảnh hưởng trực tiếp đến trải nghiệm người dùng — nút bấm nhảy chỗ, text dịch chuyển khi đang đọc. Google phạt nặng các trang có CLS cao trong mobile index.
---
- [ ] **[CRITICAL] Khai báo `width` và `height` cho tất cả thẻ ` ` và ``**
> *Tại sao:* Khi không có kích thước, trình duyệt không biết trước ảnh chiếm bao nhiêu không gian, dẫn đến toàn bộ layout bên dưới bị đẩy xuống khi ảnh load xong. Chỉ hai thuộc tính này giải quyết 60–70% lỗi CLS phổ biến.
```html
```
- [ ] **[CRITICAL] Áp dụng `aspect-ratio` CSS cho ảnh và embed responsive**
> *Tại sao:* Kết hợp với `width: 100%`, `aspect-ratio` giữ tỷ lệ khung hình và dự trữ không gian trước khi ảnh tải xong — không cần biết kích thước pixel tuyệt đối, phù hợp với responsive design hiện đại.
```css
img { width: 100%; aspect-ratio: 16 / 9; }
```
- [ ] **Đặt kích thước cố định cho iframe, embed YouTube/Maps bằng `aspect-ratio` wrapper**
> *Tại sao:* Embed từ third-party không có kích thước mặc định sẽ collapse về 0×0 rồi bung ra khi load, gây CLS nghiêm trọng. Padding-bottom hack hoặc `aspect-ratio` container giữ chỗ chính xác.
```css
.video-wrapper { aspect-ratio: 16/9; width: 100%; }
```
- [ ] **Thêm Skeleton Screen hoặc placeholder có đúng kích thước cho dynamic content**
> *Tại sao:* Nội dung tải sau (comments, recommended products, user data) xuất hiện đột ngột đẩy layout. Skeleton screen với đúng chiều cao dự kiến giữ chỗ trống trước, người dùng không thấy dịch chuyển.
- [ ] **Không chèn dynamic content phía trên existing content trừ khi do user interaction**
> *Tại sao:* Banner cookie consent, popup notification, sticky ads xuất hiện tự động sau khi page load là nguồn CLS lớn nhất. Google chỉ tha thứ CLS do user interaction (click, scroll do người dùng khởi tạo). Thiết kế các banner này dạng overlay hoặc dự trữ không gian từ đầu.
- [ ] **Preload font chữ và dùng `font-display: optional` hoặc `swap` đúng cách**
> *Tại sao:* Font swap sau khi fallback font đã render gây FOUT (Flash Of Unstyled Text) và dịch chuyển layout nếu hai font có metric khác nhau. `font-display: optional` ưu tiên không dùng font thứ 3 nếu chưa tải xong lần đầu — tốt nhất cho CLS; `swap` phù hợp khi brand font quan trọng hơn.
- [ ] **Kiểm tra và khắc phục layout shift do CSS animation/transition**
> *Tại sao:* Animation thay đổi `width`, `height`, `top`, `margin`, `padding` đều trigger layout recalculation và có thể tích lũy CLS. Chỉ dùng `transform` và `opacity` để animate — hai thuộc tính này chạy trên GPU compositor thread, không gây layout shift.
---
## PHẦN 3 — ⚡ INP Improvement (Interaction to Next Paint)
> **Ngưỡng đạt:** INP ≤ 200ms (thay thế hoàn toàn FID từ 2024, đo tất cả interactions)
> INP đo thời gian từ lúc người dùng click/tap/key cho đến khi trình duyệt vẽ lại frame tiếp theo. Main thread bận = INP cao = người dùng cảm thấy trang "lag".
---
- [ ] **[CRITICAL] Defer hoặc async tất cả JavaScript không cần thiết cho initial render**
> *Tại sao:* Script blocking chiếm main thread trong khi parse và execute, khiến mọi interaction trong thời gian đó bị queue lại. `defer` đảm bảo script chạy sau khi HTML parse xong; `async` chạy ngay khi download xong. Giảm Total Blocking Time (TBT) trực tiếp cải thiện INP.
```html
```
- [ ] **[CRITICAL] Chia nhỏ Long Tasks (>50ms) thành các task nhỏ hơn dùng `scheduler.yield()`**
> *Tại sao:* Khi main thread bận xử lý một task dài >50ms, mọi interaction phải chờ task đó hoàn thành. `scheduler.yield()` (hoặc fallback `setTimeout(0)`) nhường quyền kiểm soát về browser, cho phép user input được xử lý giữa chừng.
```js
async function processLargeList(items) {
for (const item of items) {
process(item);
await scheduler.yield(); // nhường main thread
}
}
```
- [ ] **Lazy load các component nặng (modal, chart, editor) chỉ khi cần**
> *Tại sao:* Load toàn bộ JavaScript của rich text editor hay D3 chart ngay từ đầu dù người dùng chưa cần là lãng phí. Dynamic import (`import()`) cho phép tải code on-demand, giảm parse time ban đầu, cải thiện INP cho các interaction đầu tiên.
```js
button.addEventListener('click', async () => {
const { openModal } = await import('./modal.js');
openModal();
});
```
- [ ] **Tối ưu event handler — tránh thực hiện heavy work trực tiếp trong handler**
> *Tại sao:* Event handler chạy trên main thread. Nếu handler thực hiện DOM query phức tạp, vòng lặp nặng, hay sync XHR thì toàn bộ thời gian đó tính vào INP. Dùng debounce/throttle cho scroll/resize, đẩy heavy computation sang Web Worker.
- [ ] **Giảm kích thước DOM — duy trì dưới 1.400 nodes, độ sâu tối đa 32 cấp**
> *Tại sao:* DOM lớn làm chậm mọi thao tác: style recalculation, layout, paint đều tốn nhiều thời gian hơn khi có nhiều node. Mỗi interaction trigger style recalculation — DOM 5.000 nodes có thể chậm hơn 10x so với DOM 500 nodes. Dùng virtual list cho danh sách dài.
- [ ] **Kiểm tra và loại bỏ JavaScript của third-party gây blocking (chat widget, A/B testing)**
> *Tại sao:* Third-party scripts (Intercom, Hotjar, Optimizely) thường inject nhiều code vào main thread mà bạn không kiểm soát được. Audit với Chrome DevTools → Performance → Bottom-Up để xem script nào chiếm CPU nhiều nhất, dùng Partytown hoặc Web Worker để offload.
---
## PHẦN 4 — 🎨 Ảnh & Format
> **Nguyên tắc:** Ảnh chiếm 50–80% dung lượng trang web. Tối ưu ảnh là ROI cao nhất trong web performance.
> Mỗi giây load chậm hơn do ảnh nặng = tỷ lệ bounce tăng 32% (theo Google/Deloitte 2023).
---
- [ ] **[CRITICAL] Chuyển đổi toàn bộ ảnh sang WebP, AVIF với fallback JPEG/PNG**
> *Tại sao:* AVIF nhỏ hơn JPEG 50% và WebP 20% ở cùng chất lượng. WebP nhỏ hơn JPEG 25–34%. Hỗ trợ trình duyệt: WebP 97%+, AVIF 90%+. Dùng `` với AVIF → WebP → JPEG fallback để tối ưu cho mọi trình duyệt.
```html
```
- [ ] **[CRITICAL] Implement `srcset` và `sizes` cho tất cả ảnh responsive**
> *Tại sao:* Phục vụ ảnh 1920px cho màn hình mobile 375px là lãng phí 5–10x băng thông. `srcset` cho phép trình duyệt chọn kích thước phù hợp nhất dựa trên device pixel ratio và viewport. Giảm trung bình 60–70% dung lượng ảnh trên mobile.
```html
```
- [ ] **Áp dụng `loading="lazy"` cho tất cả ảnh below-the-fold**
> *Tại sao:* Người dùng thường không cuộn đến cuối trang. Lazy loading trì hoãn tải ảnh chưa cần, giảm dữ liệu truyền tải ban đầu, giải phóng băng thông cho LCP image và critical resources. Hỗ trợ native 95%+ trình duyệt, không cần JS.
- [ ] **Nén ảnh đạt chuẩn: JPEG/WebP quality 75–85%, AVIF quality 60–75%**
> *Tại sao:* Quality cao hơn không nhất thiết nhìn tốt hơn với mắt người nhưng file nặng hơn nhiều. Quality 80 WebP thường không phân biệt được với Quality 95 JPEG nhưng nhỏ hơn 40%. Dùng Squoosh, Sharp, hoặc Imagemin để batch compress với lossy + lossless optimization.
- [ ] **Triển khai Image CDN (Cloudinary, Imgix, Cloudflare Images) để auto-optimize**
> *Tại sao:* Image CDN tự động phục vụ đúng format (AVIF/WebP), đúng kích thước dựa trên `Accept` header và `Client Hints`, cache ở edge gần người dùng, và transform on-the-fly. Thay thế cho toàn bộ pipeline xử lý ảnh thủ công.
- [ ] **Không scale ảnh bằng CSS — phục vụ đúng kích thước intrinsic**
> *Tại sao:* Hiển thị ảnh 2000×2000px trong ` ` với CSS `width: 200px` vẫn tải full 2000px về máy người dùng. Trình duyệt scale down khi render nhưng bandwidth đã mất. Luôn generate ảnh đúng kích thước cần dùng.
- [ ] **Sử dụng `decoding="async"` cho ảnh không cần thiết ngay lập tức**
> *Tại sao:* Mặc định trình duyệt decode ảnh synchronously, blocking main thread. `decoding="async"` cho phép decode off-thread, không chặn rendering. Đặc biệt hữu ích cho galleries hoặc trang nhiều ảnh.
```html
```
- [ ] **Implement Content Security Policy (CSP) cho ảnh và dùng Subresource Integrity nếu cần**
> *Tại sao:* Ngoài performance, ảnh từ third-party domain không đáng tin có thể bị inject nội dung độc hại. CSP `img-src` directive giới hạn nguồn ảnh được phép, tăng bảo mật. Google Core Update 2025 tính đến page security signals trong ranking.
---
## PHẦN 5 — 🔍 Audit & Monitoring
> **Nguyên tắc:** "Bạn không thể cải thiện những gì bạn không đo lường."
> Field data (dữ liệu người dùng thật) quan trọng hơn lab data (Lighthouse). Google dùng CrUX field data để xếp hạng, không phải Lighthouse score.
---
- [ ] **[CRITICAL] Thiết lập monitoring CrUX (Chrome UX Report) cho domain của bạn**
> *Tại sao:* CrUX là nguồn dữ liệu duy nhất Google dùng để đánh giá Core Web Vitals cho Search ranking. CrUX thu thập dữ liệu thật từ Chrome users trong 28 ngày qua. Kiểm tra tại [crux.run](https://crux.run) hoặc Google Search Console → Core Web Vitals report để biết site thực sự đang ở đâu.
- [ ] **Chạy PageSpeed Insights (PSI) cho cả mobile và desktop sau mỗi thay đổi quan trọng**
> *Tại sao:* PSI kết hợp cả lab data (Lighthouse) và field data (CrUX) trong một report duy nhất. Mobile score quan trọng hơn vì Google dùng Mobile-First Indexing. URL: [pagespeed.web.dev](https://pagespeed.web.dev). Lưu ý: PSI score 100 không đảm bảo field data tốt.
- [ ] **Integrate Lighthouse CI vào pipeline CI/CD để ngăn performance regression**
> *Tại sao:* Performance regressions thường xảy ra do code thay đổi nhỏ: thêm một library, đổi cách load font, thêm third-party script. Lighthouse CI tự động chạy test trên mỗi PR và fail build nếu LCP/CLS/INP vượt ngưỡng, ngăn vấn đề lên production.
```yaml
# .github/workflows/lighthouse.yml
- uses: treosh/lighthouse-ci-action@v11
with:
urls: 'https://example.com/'
budgetPath: './budget.json'
```
- [ ] **Triển khai Real User Monitoring (RUM) để thu thập field data chi tiết**
> *Tại sao:* Lighthouse chỉ đo một lần trong điều kiện giả lập. RUM thu thập dữ liệu từ mọi user thật, phát hiện vấn đề theo geography, device type, connection speed, browser. Dùng `web-vitals` library của Google (miễn phí) gửi về Google Analytics 4 hoặc custom endpoint.
```js
import { onLCP, onCLS, onINP } from 'web-vitals';
onLCP(metric => sendToAnalytics(metric));
onCLS(metric => sendToAnalytics(metric));
onINP(metric => sendToAnalytics(metric));
```
- [ ] **Kiểm tra Core Web Vitals trong Google Search Console hàng tuần và xử lý URLs "Poor"**
> *Tại sao:* Search Console phân loại URLs thành Good/Needs Improvement/Poor dựa trên CrUX data của từng URL. URLs "Poor" có thể bị demote trong kết quả tìm kiếm. Report này cũng nhóm URLs theo vấn đề (LCP issue, CLS issue) giúp ưu tiên fix hiệu quả hơn.
---
## 📊 Bảng Tổng Kết Mức Độ Ưu Tiên
| Phần | Số Items | Items CRITICAL | Impact |
|------|----------|----------------|--------|
| LCP Optimization | 8 | 3 | 🔴 Rất cao |
| CLS Prevention | 7 | 2 | 🟠 Cao |
| INP Improvement | 6 | 2 | 🟠 Cao |
| Ảnh & Format | 8 | 2 | 🟡 Trung bình-Cao |
| Audit & Monitoring | 5 | 1 | 🟡 Trung bình |
| **Tổng** | **34** | **10** | — |
---
## 🎯 Quick Win — Làm Ngay Trong 1 Giờ
Nếu chỉ có 1 giờ, thực hiện 5 việc này trước:
```
1. [ ] Thêm fetchpriority="high" vào LCP image
2. [ ] Thêm width/height cho tất cả
3. [ ] Chuyển ảnh sang WebP (dùng Squoosh batch)
4. [ ] Thêm loading="lazy" cho ảnh below-the-fold
5. [ ] Chạy PageSpeed Insights để baseline
```
---
## 📚 Công Cụ Tham Khảo
| Công Cụ | Mục Đích | Link |
|---------|----------|------|
| PageSpeed Insights | Lab + Field data | pagespeed.web.dev |
| WebPageTest | Deep dive lab testing | webpagetest.org |
| CrUX Dashboard | Field data trending | g.co/chromeuxdash |
| Squoosh | Compress & convert ảnh | squoosh.app |
| web-vitals JS | RUM library | npm: web-vitals |
| Lighthouse CI | CI/CD integration | github: GoogleChrome/lighthouse-ci |
---
*Cập nhật lần cuối: 2026 — Dựa trên Google Search documentation, web.dev/vitals và Chrome team announcements*