# ✅ Core Web Vitals Checklist 2026 > **Cập nhật:** Tháng 1/2026 | **Phiên bản:** 3.0 | **Nguồn:** Google Web Vitals Guidelines > **Hướng dẫn:** Đánh dấu `[x]` vào từng mục sau khi hoàn thành kiểm tra --- ## 📊 Tổng Quan Ngưỡng Mục Tiêu 2026 | Metric | ✅ Good | ⚠️ Needs Improvement | ❌ Poor | |--------|---------|----------------------|---------| | **LCP** | < 2.0s | 2.0s – 4.0s | > 4.0s | | **INP** | < 150ms | 150ms – 500ms | > 500ms | | **CLS** | < 0.08 | 0.08 – 0.25 | > 0.25 | --- ## 🖼️ Phần 1: Kiểm Tra LCP (Largest Contentful Paint) > **Mục tiêu:** LCP < 2.0 giây — Đo tốc độ tải phần tử lớn nhất trong viewport ### 1.1 Tối Ưu Hình Ảnh Hero - [ ] **Ảnh hero sử dụng định dạng WebP** — Kích thước dưới 80KB, không dùng PNG/JPEG cho ảnh hero chính ```html Hero image Hero image ``` - [ ] **Thuộc tính `fetchpriority="high"` được áp dụng** — Chỉ áp dụng cho **duy nhất 1** ảnh LCP candidate ```html Hero banner ``` ### 1.2 Hạ Tầng & Phân Phối Nội Dung - [ ] **CDN/Edge Cache được cấu hình** — Tất cả static assets (ảnh, CSS, JS, fonts) phục vụ qua CDN với cache headers phù hợp ```http # Headers kiểm tra trên CDN Cache-Control: public, max-age=31536000, immutable Vary: Accept-Encoding X-Cache: HIT ``` - [ ] **TTFB (Time to First Byte) dưới 600ms** — Đo bằng `curl` hoặc WebPageTest ```bash # Kiểm tra TTFB bằng curl curl -o /dev/null -s -w \ "TTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" \ https://example.com # Mục tiêu: time_starttransfer < 0.600 ``` - [ ] **Server Response Time tối ưu** — Cấu hình server-side caching (Redis/Memcached), database query optimization, và gzip/Brotli compression ```nginx # Nginx — Brotli compression brotli on; brotli_comp_level 6; brotli_types text/html text/css application/javascript; # Gzip fallback gzip on; gzip_vary on; gzip_types text/plain text/css application/json application/javascript; ``` ### 1.3 Loại Bỏ Điểm Nghẽn Render - [ ] **Loại bỏ render-blocking resources** — CSS quan trọng inline vào ``, CSS không quan trọng load async ```html ``` - [ ] **Preload các key assets** — Preload LCP image, critical CSS, và JavaScript quan trọng ```html ``` - [ ] **Font preload được thiết lập** — Preload các font chữ sử dụng trong above-the-fold content ```html ``` --- ## 📐 Phần 2: Kiểm Tra CLS (Cumulative Layout Shift) > **Mục tiêu:** CLS < 0.08 — Đo mức độ ổn định bố cục trang khi tải ### 2.1 Kích Thước Phần Tử Cố Định - [ ] **Tất cả thẻ `` có thuộc tính `width` và `height`** — Bắt buộc để browser tính toán aspect-ratio trước khi tải ảnh ```html Sản phẩm Sản phẩm ``` - [ ] **`aspect-ratio` CSS cho responsive images** — Giữ tỷ lệ khung hình khi ảnh responsive co giãn ```css /* ✅ Cách 1: CSS aspect-ratio (hiện đại) */ img { width: 100%; height: auto; aspect-ratio: 16 / 9; /* Giữ tỷ lệ khi responsive */ } /* ✅ Cách 2: Padding-hack (fallback cho browser cũ) */ .image-wrapper { position: relative; padding-top: 56.25%; /* 9/16 = 56.25% cho tỷ lệ 16:9 */ } .image-wrapper img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; } ``` ### 2.2 Tối Ưu Font & Typography - [ ] **`font-display: swap` hoặc `optional` cho tất cả custom fonts** — Tránh FOIT (Flash of Invisible Text) và giảm thiểu layout shift do font ```css @font-face { font-family: 'CustomFont'; src: url('/fonts/custom.woff2') format('woff2'); /* ✅ swap: hiện text ngay với fallback font */ font-display: swap; /* Hoặc dùng 'optional' nếu font không quan trọng */ /* font-display: optional; */ font-weight: 400; font-style: normal; } ``` ### 2.3 Quảng Cáo & Nội Dung Động - [ ] **Ad placeholder có `min-height` cố định** — Dành sẵn không gian cho quảng cáo trước khi load ```html
``` - [ ] **Không inject nội dung phía trên existing content** — Không dùng JavaScript để chèn banner/notification/cookie bar đẩy nội dung xuống ```javascript // ❌ Sai — inject nội dung đẩy layout xuống document.body.insertBefore(newBanner, document.body.firstChild); // ✅ Đúng — dành sẵn không gian trong HTML // Dùng position: fixed hoặc reserved space const notification = document.getElementById('notification-placeholder'); notification.innerHTML = bannerContent; // Thay thế placeholder đã có sẵn ``` ### 2.4 Animation & Chuyển Động - [ ] **Animation chỉ sử dụng `transform` và `opacity`** — Tránh animate các property gây reflow (width, height, top, left, margin, padding) ```css /* ❌ Sai — gây layout shift và reflow */ .bad-animation { transition: width 0.3s, height 0.3s, top 0.3s, margin 0.3s; } /* ✅ Đúng — chỉ dùng transform và opacity (GPU-accelerated) */ .good-animation { transition: transform 0.3s ease, opacity 0.3s ease; will-change: transform; /* Hint cho browser tạo composite layer */ } .slide-in { transform: translateX(-100%); opacity: 0; } .slide-in.active { transform: translateX(0); opacity: 1; } ``` ### 2.5 Iframe & Embedded Content - [ ] **Tất cả `
``` --- ## ⚡ Phần 3: Kiểm Tra INP (Interaction to Next Paint) > **Mục tiêu:** INP < 150ms — Đo thời gian phản hồi với mọi tương tác người dùng ### 3.1 Tối Ưu JavaScript Execution - [ ] **Không có long tasks vượt quá 50ms** — Kiểm tra trong Chrome DevTools > Performance tab, tìm tasks màu đỏ ```javascript // ❌ Sai — Long task chặn main thread function processLargeData(data) { return data.map(item => heavyComputation(item)); // Có thể > 50ms } // ✅ Đúng — Chia nhỏ bằng scheduler hoặc chunking async function processLargeDataOptimized(data) { const CHUNK_SIZE = 50; const results = []; for (let i = 0; i < data.length; i += CHUNK_SIZE) { const chunk = data.slice(i, i + CHUNK_SIZE); results.push(...chunk.map(item => heavyComputation(item))); // Yield control về main thread sau mỗi chunk await scheduler.yield(); // Chrome 115+ // Fallback: await new Promise(resolve => setTimeout(resolve, 0)); } return results; } ``` - [ ] **Scripts analytics có thuộc tính `defer`** — Analytics không cần chạy ngay, luôn dùng `defer` hoặc `async` ```html ``` ### 3.2 Code Splitting & Lazy Loading - [ ] **Code splitting được áp dụng cho routes** — Chỉ tải JavaScript cần thiết cho trang hiện tại ```javascript // ✅ React — Dynamic import với React.lazy import { lazy, Suspense } from 'react'; const HeavyComponent = lazy(() => import('./HeavyComponent')); const AdminDashboard = lazy(() => import('./pages/AdminDashboard')); function App() { return ( Đang tải...}> } /> } /> ); } // ✅ Vanilla JS — Dynamic import button.addEventListener('click', async () => { const { openModal } = await import('./modal.js'); // Chỉ tải khi cần openModal(); }); ``` - [ ] **Web Workers cho tác vụ nặng** — Chuyển computation phức tạp ra khỏi main thread ```javascript // worker.js — Chạy trong background thread self.addEventListener('message', (event) => { const { data, type } = event.data; if (type === 'PROCESS_DATA') { // Tác vụ nặng: sort, filter, parse JSON lớn, crypto... const result = heavyDataProcessing(data); self.postMessage({ type: 'RESULT', result }); } }); // main.js — Không block main thread const worker = new Worker('/worker.js', { type: 'module' }); worker.postMessage({ type: 'PROCESS_DATA', data: largeDataset }); worker.addEventListener('message', (event) => { if (event.data.type === 'RESULT') { updateUI(event.data.result); // Cập nhật UI sau khi xử lý xong } }); ``` ### 3.3 Tối Ưu Event Listeners - [ ] **Event listeners được debounced/throttled** — Tránh xử lý quá nhiều events liên tiếp ```javascript // ✅ Debounce — Chỉ thực thi sau khi user dừng tương tác function debounce(fn, delay = 300) { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn.apply(this, args), delay); }; } // ✅ Throttle — Thực thi tối đa 1 lần trong khoảng thời gian function throttle(fn, limit = 100) { let inThrottle; return (...args) => { if (!inThrottle) { fn.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } // Áp dụng const searchInput = document.getElementById('search'); searchInput.addEventListener('input', debounce(handleSearch, 300)); window.addEventListener('scroll', throttle(handleScroll, 100)); window.addEventListener('resize', debounce(handleResize, 200)); ``` - [ ] **Third-party scripts được lazy load** — Chỉ tải chatbot, social widgets, video players khi cần thiết ```javascript // ✅ Load chat widget chỉ khi user scroll xuống 50% const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { loadChatWidget(); observer.disconnect(); } }); }, { threshold: 0.1 }); observer.observe(document.getElementById('chat-trigger')); function loadChatWidget() { const script = document.createElement('script'); script.src = 'https://widget.chat-provider.com/chat.js'; script.async = true; document.head.appendChild(script); } // ✅ Load social share buttons khi hover document.querySelector('.share-section').addEventListener('mouseenter', () => { import('./social-share.js').then(module => module.initShareButtons()); }, { once: true }); ``` --- ## 🛠️ Phần 4: Công Cụ Đo Lường > **Lưu ý:** Sử dụng ít nhất **3 công cụ** để có kết quả toàn diện — Lab data và Field data bổ sung cho nhau ### 4.1 Công Cụ Chính Thức Google - [ ] **PageSpeed Insights** — Kiểm tra cả Lab data và Field data (CrUX) ``` 🔗 URL: https://pagespeed.web.dev/ 📋 Cách sử dụng: 1. Nhập URL cần kiểm tra 2. Xem tab "Mobile" và "Desktop" riêng biệt 3. Chú ý phần "Field Data" (dữ liệu thực tế từ người dùng) 4. Xem "Diagnostics" để biết nguyên nhân cụ thể ✅ Mục tiêu điểm số: - Performance: ≥ 90 - LCP: < 2.0s (màu xanh) - INP: < 150ms (màu xanh) - CLS: < 0.08 (màu xanh) ``` - [ ] **Google Lighthouse** — Kiểm tra chi tiết trong môi trường lab ```bash # Cài đặt Lighthouse CLI npm install -g lighthouse # Chạy audit và xuất báo cáo HTML lighthouse https://example.com \ --output=html \ --output-path=./lighthouse-report.html \ --preset=desktop \ --throttling-method=simulate # Chạy cho mobile (mặc định) lighthouse https://example.com \ --output=json \ --output-path=./lighthouse-mobile.json # Hoặc dùng trực tiếp trong Chrome: # DevTools (F12) > Lighthouse tab > Analyze page load ``` ### 4.2 Công Cụ Monitoring Thực Tế - [ ] **Web Vitals Extension** — Đo real-time trên browser của bạn ``` 🔗 Cài đặt: Chrome Web Store > "Web Vitals" 📋 Tính năng: - Hiển thị LCP, INP, CLS real-time góc phải màn hình - Màu xanh = Good | Vàng = Needs Improvement | Đỏ = Poor - Click icon để xem breakdown chi tiết - Bật "Console logging" để debug trong DevTools ⌨️ Đọc dữ liệu trong Console: ``` ```javascript // Tự đo bằng Web Vitals library import { onLCP, onINP, onCLS } from 'web-vitals'; onLCP(metric => console.log('LCP:', metric.value, 'ms')); onINP(metric => console.log('INP:', metric.value, 'ms')); onCLS(metric => console.log('CLS:', metric.value)); ``` - [ ] **Google Search Console** — Theo dõi Core Web Vitals của toàn bộ website theo thời gian ``` 🔗 URL: https://search.google.com/search-console/ 📋 Điều hướng: Experience > Core Web Vitals > Xem báo cáo Mobile/Desktop ✅ Kiểm tra: □ Số URL trong nhóm "Good" đang tăng □ Không có URL nào trong nhóm "Poor" □ Xem "Issues" để biết nhóm URL cần fix □ So sánh trend theo tháng ⏰ Lưu ý: Dữ liệu cập nhật sau 28 ngày (dùng CrUX field data) ``` - [ ] **CrUX Dashboard (Chrome UX Report)** — Dữ liệu thực tế từ người dùng Chrome theo lịch sử ``` 🔗 URL: https://lookerstudio.google.com/c/u/0/reporting/bbc5698d-57bb-4969-9e07-68810b9fa348 📋 Cách sử dụng: 1. Mở dashboard > Nhập domain vào ô "Origin" 2. Chọn khoảng thời gian muốn xem (theo tháng) 3. So sánh Mobile vs Desktop performance 4. Theo dõi P75 (percentile 75) — đây là ngưỡng Google đánh giá 📊 Chỉ số cần theo dõi: □ % người dùng có LCP "Good" > 75% □ % người dùng có INP "Good" > 75% □ % người dùng có CLS "Good" > 75% 💡 Tip: Export data ra Sheets để track theo thời gian ``` --- ## 🎯 Phần 5: Ngưỡng Mục Tiêu 2026 > Nguồn: [web.dev/vitals](https://web.dev/vitals) — Cập nhật Q1/2026 ### 5.1 Bảng Ngưỡng Chi Tiết | Metric | Mô Tả | 🟢 Good | 🟡 Needs Improvement | 🔴 Poor | Đo tại | |--------|--------|---------|----------------------|---------|--------| | **LCP** | Largest Contentful Paint | **< 2.0s** | 2.0s – 4.0s | > 4.0s | P75 | | **INP** | Interaction to Next Paint | **< 150ms** | 150ms – 500ms | > 500ms | P75 | | **CLS** | Cumulative Layout Shift | **< 0.08** | 0.08 – 0.25 | > 0.25 | P75 | ### 5.2 Checklist Xác Nhận Ngưỡng - [ ] **LCP < 2.0 giây** — Đo trên mobile với mạng 4G thực tế (không chỉ lab) ``` ✅ Kiểm tra: PageSpeed Insights > Field Data > LCP (màu xanh lá) ✅ Target: 75% người dùng có LCP dưới 2.0s 💡 Quick wins: Tối ưu ảnh hero + CDN thường giảm 40-60% LCP ``` - [ ] **INP < 150 milli-giây** — Đo với tương tác thực tế (click, tap, keyboard) ``` ✅ Kiểm tra: Web Vitals Extension khi dùng trang bình thường ✅ Target: 75% interactions hoàn thành trong 150ms 💡 Quick wins: Loại bỏ long tasks + defer analytics = cải thiện ngay ``` - [ ] **CLS < 0.08** — Đo trong suốt vòng đời trang (không chỉ lúc load) ``` ✅ Kiểm tra: Lighthouse > CLS score + xem "Avoid large layout shifts" ✅ Target: Tổng điểm CLS dưới 0.08 (thấp hơn ngưỡng 2025 là 0.1) 💡 Quick wins: Thêm width/height cho ảnh + font-display swap ``` ### 5.3 Scoring Summary - [ ] **Tất cả 3 metrics đạt ngưỡng "Good"** trên cả Mobile lẫn Desktop - [ ] **PageSpeed Performance Score ≥ 90** trên Mobile - [ ] **PageSpeed Performance Score ≥ 95** trên Desktop - [ ] **Không có URL nào** trong Search Console báo "Poor" status - [ ] **CrUX Field Data** xác nhận ≥ 75% người dùng thực đạt "Good" cho cả 3 metrics --- ## 📝 Ghi Chú & Tài Nguyên ### 🔗 Tài Nguyên Tham Khảo | Tài Nguyên | URL | |------------|-----| | Web Vitals Official | `https://web.dev/vitals` | | PageSpeed Insights | `https://pagespeed.web.dev` | | CrUX Dashboard | `https://g.co/chromeuxdash` | | Search Console | `https://search.google.com/search-console` | | Lighthouse Docs | `https://developer.chrome.com/docs/lighthouse` | | Web Vitals JS Library | `https://github.com/GoogleChrome/web-vitals` | ### 📅 Lịch Kiểm Tra Khuyến Nghị | Tần Suất | Công Cụ | Mục Đích | |----------|---------|----------| | **Hàng ngày** | Web Vitals Extension | Spot-check khi deploy | | **Hàng tuần** | PageSpeed Insights | Theo dõi trend | | **Hàng tháng** | CrUX Dashboard | Phân tích field data | | **Mỗi release** | Lighthouse CI | Automated testing | ### ⚠️ Lưu Ý Quan Trọng > - **Field Data > Lab Data:** Luôn ưu tiên dữ liệu thực tế từ người dùng hơn kết quả lab > - **Mobile First:** Google đánh giá ranking dựa trên Core Web Vitals của mobile > - **P75 là chuẩn:** Google dùng percentile 75 — 75% người dùng phải đạt ngưỡng "Good" > - **INP thay FID:** Từ tháng 3/2024, INP chính thức thay thế FID trong ranking signals --- *Checklist này nên được review và cập nhật mỗi quý theo thông báo từ [Chrome Developers Blog](https://developer.chrome.com/blog)*