Web Performance · Part 15 — Rendering & Delivery Strategies
CSR, SSR, SSG, ISR, streaming SSR và islands/partial/progressive hydration ảnh hưởng LCP, INP, CLS, TTFB ra sao — và khung quyết định chọn kiến trúc render theo mục tiêu hiệu năng.
Tối ưu vi mô (ảnh, font, bundle) có trần. Đòn bẩy lớn nhất thường là kiến trúc render — bạn dựng HTML ở đâu (server hay client) và “tưới sống” (hydrate) ra sao. Phần này so sánh các chiến lược render qua lăng kính Core Web Vitals, để bạn chọn đúng cho từng loại trang thay vì theo trào lưu framework.
Câu hỏi trung tâm: HTML có nội dung tới mắt người dùng nhanh thế nào (LCP/TTFB), và trang phản hồi tương tác nhanh thế nào (INP)? Mỗi chiến lược đánh đổi hai điều này khác nhau.
1. Bốn cách dựng HTML
CSR (Client-Side Rendering)
Server gửi document shell + JS → JS fetch dữ liệu và dựng phần nội dung chính
Document có thể nhỏ, nhưng LCP thường phải chờ mạng + JS; CPU client cao
SSR (Server-Side Rendering)
Server dựng HTML đầy đủ mỗi request → client hydrate
LCP nhanh hơn (HTML có sẵn), TTFB phụ thuộc tốc độ server
SSG (Static Site Generation)
Dựng HTML lúc BUILD → phục vụ tĩnh qua CDN
TTFB thường thấp; nội dung build-time cần rebuild/revalidate, phần client vẫn có thể động
ISR (Incremental Static Regeneration)
Pattern cache của một số framework: phục vụ bản tĩnh rồi tái sinh theo thời gian/nhu cầu
Nhanh khi cache hit, đổi lại có cửa sổ stale và bài toán invalidation
| Chiến lược | TTFB thường gặp | LCP | Dữ liệu | CPU client |
|---|---|---|---|---|
| CSR | shell có thể nhanh | dễ trễ nếu nội dung chờ JS | runtime | cao nếu render nhiều |
| SSR | phụ thuộc server/cache | có lợi nếu HTML + asset tới sớm | theo request | phụ thuộc hydration |
| SSG | thấp khi ở CDN | có lợi, không tự động bảo đảm | build-time + client fetch tùy app | từ gần 0 tới cao |
| ISR | thấp khi cache hit | gần SSG khi cache hit | gần-tươi, theo policy | phụ thuộc hydration |
Quy tắc thô: nội dung tĩnh/ít đổi (blog, docs, marketing) → SSG hoặc cache/revalidation; cá nhân hóa cao/dữ liệu tươi → SSR hay hybrid; app sau đăng nhập, SEO không quan trọng → CSR có thể chấp nhận. Không chiến lược nào tự động cho CWV tốt: CSS, ảnh LCP, cache, data dependency và lượng JS vẫn quyết định kết quả.
2. Vấn đề hydration — “uncanny valley”
SSR/SSG có thể trả HTML nhanh. Phần HTML tĩnh không cần hydrate; chỉ UI có runtime client mới cần JS gắn listener và khôi phục state. Với hydration toàn trang, UI có thể trông sẵn sàng trước khi handler sẵn sàng; tương tác trong khoảng này có thể bị trì hoãn, replay hoặc mất tùy framework — vì vậy phải test user flow thật thay vì suy từ ảnh chụp LCP.
SSR truyền thống:
[HTML hiện nhanh — LCP tốt] ──── [JS tải] ──── [hydrate CẢ trang] ── tương tác được
người dùng bấm ở đây → không phản hồi (INP xấu)
Hydrate toàn trang nghĩa là tải + chạy phần lớn cây component dù nhiều vùng chỉ hiển thị nội dung. Nó có thể tạo long task và cạnh tranh với tương tác, là một nguyên nhân phổ biến của INP xấu trong app SSR nhiều JavaScript.
3. Các kiểu hydration giảm chi phí
① Islands (Astro, Fresh)
Trang chủ yếu HTML tĩnh; chỉ vài "đảo" tương tác được hydrate.
→ JS tối thiểu, INP tốt cho trang nội dung.
② Partial / Selective hydration
Hydrate component theo nhu cầu, không phải tất cả cùng lúc.
③ Progressive hydration
Hydrate dần theo độ ưu tiên / khi cuộn tới (lazy).
④ Server Components (ví dụ React Server Components)
Tách component chỉ chạy server khỏi ranh giới client; code server không vào
client bundle, còn Client Components vẫn cần hydrate.
Tên gọi partial/selective/progressive không được các framework dùng hoàn toàn giống nhau. Tư duy chung vẫn là gửi ít JS hơn, hydrate ít hơn và ưu tiên đúng vùng. Islands là chiến lược hydration; Server Components là ranh giới thực thi/server-client rộng hơn, không nên đồng nhất hai khái niệm.
4. Streaming SSR — trả HTML theo dòng
Thay vì chờ server dựng toàn bộ HTML rồi mới gửi, streaming SSR có thể flush shell/<head> sớm rồi stream phần cần dữ liệu khi sẵn sàng. Nó giảm thời gian tới nội dung đầu tiên khi server thực sự flush sớm; nếu proxy buffer response hoặc shell cũng chờ dữ liệu, lợi ích biến mất.
Không streaming: request → [chờ MỌI dữ liệu 600ms] → gửi cả HTML
Streaming: request → gửi shell ngay (TTFB thấp)
→ stream phần A khi query A xong
→ stream phần B khi query B xong
React (renderToPipeableStream) + <Suspense> cho phép bao phần chậm: shell + nội dung nhanh hiện trước, phần chậm stream sau với fallback. Nếu phần tử LCP thật nằm trong boundary chậm, fallback sớm không tự làm LCP tốt; phải đo candidate và element render delay.
5. Ảnh hưởng tới từng Core Web Vital
LCP: SSG/SSR/streaming giúp nội dung được khám phá sớm, nhưng còn phụ thuộc
TTFB, CSS, ảnh/font và boundary chứa candidate. CSR dễ thêm chuỗi chờ JS.
INP: hydration nặng có thể là nguồn main-thread work lớn. Ít JS / islands / RSC / progressive
hydration → INP tốt hơn. Chia nhỏ task (Part 16).
CLS: SSR/SSG dễ bị CLS nếu hydration thay đổi layout, hoặc nội dung
stream chèn vào đẩy layout. Đặt chỗ trước (reserve space), tránh
chèn nội dung phía trên (Part 7).
TTFB: SSG/CDN thường thấp; SSR phụ thuộc server/cache/data source.
Streaming có thể đưa byte đầu/shell tới sớm nhưng không sửa backend chậm.
Bẫy CLS với streaming: nội dung stream vào sau phải có chỗ đặt trước, nếu không nó đẩy phần đã hiện → layout shift. Streaming cải thiện LCP nhưng cần kỷ luật CLS.
6. Khung quyết định
Trang nội dung tĩnh (blog/docs/landing)
→ SSG/ISR + islands. JS tối thiểu. LCP & INP đều tốt.
Trang động, discoverability quan trọng (e-commerce, tin tức)
→ SSR/cache revalidation hoặc hybrid; streaming khi dependency chậm không đều.
RSC chỉ là lựa chọn khi stack React phù hợp, không phải yêu cầu.
App sau đăng nhập (dashboard), SEO không cần
→ CSR/SPA chấp nhận được, nhưng vẫn tối ưu bundle & INP.
Dữ liệu chậm không đồng đều
→ streaming SSR + Suspense (shell nhanh, phần chậm stream sau).
Đừng chọn kiến trúc theo framework “hot”; chọn theo đặc tính trang và mục tiêu metric. Một site thường trộn nhiều chiến lược (marketing = SSG, app = CSR, checkout = SSR).
7. Tóm tắt
- CSR dễ thêm chuỗi chờ JS cho LCP; SSR đưa HTML nội dung tới sớm nhưng TTFB theo server; SSG/cache revalidation thường có lợi khi CDN hit. Không lựa chọn nào tự bảo đảm CWV.
- Hydration toàn trang có thể tạo long task và khoảng UI trông sẵn sàng trước handler; HTML tĩnh thì không cần hydrate.
- Giảm chi phí bằng islands, partial/progressive hydration, Server Components (RSC) — gửi ít JS, hydrate ít.
- Streaming SSR + Suspense có thể trả shell sớm rồi stream phần chậm; lợi ích LCP phụ thuộc candidate, và cần đặt chỗ trước để tránh CLS.
- Chọn kiến trúc theo đặc tính trang + mục tiêu metric, không theo trào lưu; trộn nhiều chiến lược trong một site là bình thường.
Phần tiếp theo: INP deep & main-thread scheduling — Long Animation Frames (LoAF) API, scheduler.postTask/yield, isInputPending, và chia nhỏ tác vụ dài để giữ INP “Good”.