Seminar Docs / Core Web Vitals
CORE WEB
VITALS
5 chỉ số Google dùng để đánh giá trải nghiệm người dùng thực tế — và là thước đo khách quan nhất để so sánh hiệu năng giữa SSG, SSR, ISR và CSR trong dự án này.
LCP
< 2.5s
FID
< 100ms
CLS
< 0.1
TTFB
< 800ms
TBT
< 200ms
Trọng số từng metric
Nguồn: Lighthouse v11 scoring weights
Ước tính điểm theo rendering mode
* Ước tính dựa trên benchmark thực tế. Điểm thực tế đo bằng component RenderBenchmark trên mỗi trang.
Largest Contentful Paint
SSG
~0.8s
GoodISR
~0.9s
GoodSSR
~1.8s
FairCSR
~4.5s
PoorĐo lường gì?
Thời gian để phần tử lớn nhất trên màn hình (thường là hero image hoặc heading chính) được render hoàn toàn. Đây là chỉ số người dùng cảm nhận trực tiếp nhất — "trang này load nhanh không?"
Tại sao quan trọng?
Google coi LCP là chỉ số quan trọng nhất trong Core Web Vitals. LCP kém đồng nghĩa với bounce rate cao — người dùng bỏ trang trước khi nhìn thấy nội dung.
Tối ưu trong dự án
- Dùng <Image> của Next.js với priority={true} cho hero image → trình duyệt biết preload sớm
- SSG/ISR: image URL đã có trong HTML → browser bắt đầu download ngay khi parse HTML
- Tránh lazy load image phần "above the fold" (phần nhìn thấy không cần scroll)
- Serve ảnh từ CDN cùng region (DigitalOcean Spaces SGP1 → HCM < 15ms)
So sánh theo rendering mode
First Input Delay
SSG
~8ms
GoodISR
~8ms
GoodSSR
~12ms
GoodCSR
~180ms
FairĐo lường gì?
Thời gian từ khi user lần đầu tương tác (click, tap, keypress) đến khi browser thực sự bắt đầu xử lý event handler. FID đo lường khả năng phản hồi của trang khi đang trong quá trình load.
Tại sao quan trọng?
FID cao xảy ra khi main thread đang bận parse/execute JS bundle — click của user bị xếp hàng chờ. Cảm giác trang "đơ" hoặc "không phản hồi" khi vừa mở. Google đã thay FID bằng INP (Interaction to Next Paint) từ 2024 nhưng nguyên tắc giống nhau.
Tối ưu trong dự án
- Code splitting: Next.js tự động split theo route — trang nào chỉ load JS của trang đó
- Tránh inline heavy computation trong useEffect ngay khi mount
- Lazy load component không cần thiết với dynamic import()
- CSR bundle lớn hơn vì inline toàn bộ component — main thread bị block lâu hơn
So sánh theo rendering mode
Cumulative Layout Shift
SSG
~0.02
GoodISR
~0.02
GoodSSR
~0.03
GoodCSR
~0.18
FairĐo lường gì?
Tổng điểm dịch chuyển layout không mong muốn trong suốt vòng đời trang. Xảy ra khi element trên trang bị đẩy xung quanh sau khi đã render — ví dụ: ảnh load muộn đẩy text xuống, font swap làm text nhảy.
Tại sao quan trọng?
CLS cao là trải nghiệm tệ nhất cho người dùng — đang đọc bài thì text tự nhảy, click nhầm button vì nó dịch chuyển. Google phạt nặng trang có CLS > 0.25 trong ranking.
Tối ưu trong dự án
- Luôn set width và height cho <Image> — Next.js Image tự reserve không gian trước khi ảnh load
- CSR skeleton → real content swap gây CLS: dùng skeleton có đúng height với content thật
- Font: dùng font-display: swap cẩn thận, hoặc preload font chính để tránh FOUT
- Tránh inject content phía trên existing content sau khi render (ads, banners)
So sánh theo rendering mode
Time to First Byte
SSG
~15ms
GoodISR
~20ms
GoodSSR
~350ms
FairCSR
~15ms
GoodĐo lường gì?
Thời gian từ khi browser gửi HTTP request đến khi nhận byte đầu tiên của response. TTFB bao gồm: DNS lookup + TCP connection + SSL handshake + server processing time. Với SSG/ISR: server processing gần như 0. Với SSR: bao gồm cả thời gian fetch database.
Tại sao quan trọng?
TTFB là "starting gun" của mọi metric khác — nếu TTFB cao, mọi thứ sau đó đều bị delay. Đây là điểm khác biệt rõ nhất giữa SSG và SSR trong dự án này.
Tối ưu trong dự án
- SSG/ISR TTFB thấp vì Coolify serve file tĩnh qua Traefik — không có Node.js processing
- SSR TTFB = thời gian Express query MongoDB + Mongoose hydration + Next.js render to string
- Đặt VPS cùng region với user (SGP1 cho VN) giảm TTFB ~150ms so với US region
- MongoDB trong cùng Docker network → query latency < 1ms, không ảnh hưởng nhiều đến SSR TTFB
So sánh theo rendering mode
Total Blocking Time
SSG
~40ms
GoodISR
~40ms
GoodSSR
~55ms
GoodCSR
~380ms
PoorĐo lường gì?
Tổng thời gian main thread bị block bởi các long tasks (task > 50ms). Mỗi long task, phần vượt quá 50ms được tính vào TBT. Đây là metric phản ánh trực tiếp kích thước và độ phức tạp của JS bundle.
Tại sao quan trọng?
TBT cao = trang cảm giác "đơ" khi đang load. Lighthouse dùng TBT thay cho FID trong lab testing vì FID cần user thực tế tương tác. TBT chiếm 25% điểm Lighthouse Performance.
Tối ưu trong dự án
- CSR bundle lớn hơn vì inline Navbar, Slider, Footer, BlogGrid trực tiếp vào file
- SSG/ISR/SSR import components riêng → Next.js code split thành chunks nhỏ load song song
- Tree shaking: chỉ import function cần dùng từ postUtils, không import toàn bộ
- Dùng Chrome DevTools Performance tab để xem long tasks màu đỏ trên main thread timeline
So sánh theo rendering mode
RenderBenchmark Component
Component tự build, đo real-time trực tiếp trên browser bằng PerformanceObserver API. Hiển thị FCP, LCP, TBT, TTFB, Hydration time ngay trên mỗi trang rendering.
const lcpObs = new PerformanceObserver(list => {
const last = list.getEntries().at(-1);
setMetrics(prev => ({
...prev, lcp: last.startTime
}));
});
lcpObs.observe({
type: 'largest-contentful-paint',
buffered: true
});Chrome DevTools
Lighthouse tab trong Chrome DevTools chạy audit đầy đủ. Performance tab cho phép xem timeline chi tiết: long tasks (TBT), paint events (FCP/LCP), layout shifts (CLS).
// Mở Chrome DevTools
// Tab: Lighthouse
// Device: Mobile (khắt khe hơn)
// Categories: Performance
// → Generate Report
// Tab: Performance
// Record khi load trang
// Xem: Long Tasks (đỏ)
// Paint events
// Layout shiftsNavigation Timing API
Browser API built-in để đo TTFB và các timing khác. RenderBenchmark dùng API này để lấy responseStart - requestStart = TTFB thực tế của trang hiện tại.
const navEntry = performance
.getEntriesByType('navigation')[0];
const ttfb =
navEntry.responseStart -
navEntry.requestStart;
// Long Tasks → TBT
const tbtObs = new PerformanceObserver(
list => {
list.getEntries().forEach(entry => {
tbt += entry.duration - 50;
});
}
);
tbtObs.observe({ type: 'longtask' });