Web Performance · Part 4 — Critical Rendering Path & Loading
The journey from HTML to pixels: DOM, CSSOM, render tree, layout, paint. Why CSS and JS block rendering, and resource hints (preload, preconnect, prefetch, defer, async) to control load order.
Để tối ưu tốc độ tải, bạn phải hiểu trình duyệt làm gì từ lúc nhận HTML tới lúc vẽ pixel đầu tiên. Hành trình đó gọi là Critical Rendering Path (CRP). Hiểu nó giải thích vì sao một thẻ <script> đặt sai chỗ có thể làm trắng màn hình, và vì sao CSS lại “chặn render”.
1. Từ HTML tới pixel: các bước của CRP
HTML ──parse──▶ DOM ─┐
├─▶ Render Tree ─▶ Layout ─▶ Paint ─▶ Composite ─▶ pixel
CSS ──parse──▶ CSSOM ─┘
- Trình duyệt parse HTML thành DOM.
- Parse CSS thành CSSOM và tính style cho các node cần render.
- Từ DOM + style, tạo render tree (node
display: nonekhông có box render). - Layout tính vị trí và kích thước các box.
- Paint ghi các lệnh vẽ cho text, màu, ảnh và hiệu ứng.
- Compositor ghép các layer/tile thành khung hình trên màn hình.
Mục tiêu tối ưu tải: rút ngắn thời gian tới khi render tree sẵn sàng và paint lần đầu xảy ra (FCP/LCP).
2. CSS chặn render
Các stylesheet áp dụng cho media hiện tại và được phát hiện sớm thường chặn render: trình duyệt chờ tải + parse chúng để tránh vẽ nội dung chưa style (FOUC). Không phải mọi file CSS đều chặn mọi viewport; stylesheet có media query không khớp có thể được tải với ưu tiên thấp hơn mà không chặn paint hiện tại.
Hệ quả thực tế:
- Giảm CSS chặn: chỉ nạp CSS cần cho phần đầu trang (above-the-fold); phần còn lại nạp sau.
- Inline critical CSS: nhúng thẳng CSS tối thiểu cho above-the-fold vào
<head>để paint ngay, không chờ request CSS ngoài. - CSS có điều kiện không chặn: dùng
mediađể CSS chỉ chặn khi áp dụng.
<!-- These styles only block rendering when their media query matches. -->
<link rel="stylesheet" href="print.css" media="print" />
<link rel="stylesheet" href="wide.css" media="(min-width: 80rem)" />
Đừng dùng mẹo đổi media bằng onload như một mặc định: nó có thể gây FOUC/CLS và còn phụ thuộc JavaScript. Với CSS thật sự không critical, ưu tiên tách theo route/component hoặc chỉ chèn stylesheet khi tính năng cần nó.
3. JavaScript chặn parse — và async/defer
Mặc định, gặp <script> (không thuộc tính), trình duyệt dừng parse HTML, tải script, chạy nó, rồi mới parse tiếp. Vì script có thể document.write hay sửa DOM. Một script lớn ở <head> = trắng màn hình tới khi nó xong.
Hai thuộc tính giải quyết:
<!-- defer: download in parallel, RUN after HTML parse completes, preserves order. Default choice. -->
<script src="app.js" defer></script>
<!-- async: download in parallel, RUN as soon as loaded (interrupts parse), NO order guarantee. -->
<script src="analytics.js" async></script>
| Chặn parse? | Thời điểm chạy | Giữ thứ tự? | Dùng cho | |
|---|---|---|---|---|
| (none) | Có | ngay lập tức | có | hiếm khi nên dùng |
defer | Không | sau khi DOM xong | có | script ứng dụng |
async | Chỉ khi execute | ngay khi tải xong | không | script độc lập (analytics) |
type="module" | Không | sau khi parse (mặc định defer) | theo module graph | ES modules |
Quy tắc đơn giản: script ứng dụng dùng
defer(hoặctype="module", vốn defer sẵn); script bên thứ ba độc lập dùngasync. Tránh script đồng bộ trong<head>.
4. Resource Hints: kiểm soát thứ tự & thời điểm tải
Trình duyệt tự đoán thứ tự tải, nhưng bạn biết rõ hơn cái gì quan trọng. Resource hints cho phép “nói trước” với trình duyệt.
bắt tay sớm với origin khác
Kết nối tới một domain (DNS + TCP + TLS) tốn thời gian. preconnect làm bước này sớm cho các origin quan trọng (CDN font, API):
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="dns-prefetch" href="https://cdn.example.com" /> <!-- lighter — DNS only -->
tải sớm tài nguyên quan trọng bị “giấu”
Một số tài nguyên quan trọng bị trình duyệt phát hiện muộn (font khai trong CSS, ảnh hero trong CSS background, module import sâu). preload giúp phát hiện và bắt đầu tải sớm; nó không mặc định biến mọi request thành ưu tiên cao, nên có thể kết hợp fetchpriority khi thật sự cần:
<!-- preload above-the-fold font to avoid waiting -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin />
<!-- preload LCP image so it loads immediately, not after CSS/JS -->
<link rel="preload" href="/hero.avif" as="image" fetchpriority="high" />
Cẩn thận:
preloadquá nhiều thứ phản tác dụng — nó cạnh tranh băng thông với chính tài nguyên đang cần và có thể tải cả tài nguyên cuối cùng không dùng. Chỉ preload tài nguyên critical bị phát hiện muộn. Nếu ảnh LCP đã cósrc/srcsetngay trong HTML, thường bắt đầu vớifetchpriority="high"và chỉ thêm preload khi waterfall cho thấy vẫn phát hiện trễ.
tải trước cho điều hướng tương lai
prefetch tải tài nguyên cho trang có thể tới tiếp theo, với ưu tiên thấp (lúc rảnh):
<!-- user likely goes to /dashboard next → prefetch when idle -->
<link rel="prefetch" href="/dashboard.js" as="script" />
tinh chỉnh ưu tiên (hint, không phải mệnh lệnh)
<img src="/hero.avif" fetchpriority="high" width="1200" height="600" alt="..." />
<img src="/below-fold.avif" loading="lazy" width="800" height="600" alt="..." />
5. Thứ tự tối ưu CRP (tổng hợp)
Gom lại thành quy trình:
- Giảm số byte chặn render: inline critical CSS, defer JS, hoãn CSS không quan trọng.
- Rút ngắn chuỗi request quan trọng: ít vòng request nối tiếp nhau (request A xong mới biết cần B).
- Ưu tiên đúng tài nguyên:
preconnectmột vài origin quan trọng,preloadtài nguyên critical bị phát hiện muộn,fetchpriorityđể gợi ý thứ tự tương đối. - Hoãn phần không cần ngay: lazy-load ảnh dưới màn hình, code-split JS (Phần 8), prefetch điều hướng tương lai.
Dùng waterfall của WebPageTest (Phần 3) để thấy chuỗi request quan trọng và phát hiện request nào đến muộn.
Tóm tắt
- CRP: HTML→DOM, CSS→CSSOM, gộp thành render tree → layout → paint → composite. Tối ưu là rút ngắn tới paint đầu.
- CSS áp dụng cho viewport có thể chặn render → giảm/inline critical CSS có kiểm soát, tách CSS theo route/media/tính năng.
- JS chặn parse → dùng
defercho script ứng dụng,asynccho script độc lập; tránh script đồng bộ trong<head>. - Resource hints:
preconnect(bắt tay sớm),preload(phát hiện sớm tài nguyên quan trọng bị giấu — đừng lạm dụng),prefetch(điều hướng tương lai),fetchpriority(gợi ý ưu tiên). Phần 14 sẽ đào sâumodulepreload, Early Hints và Speculation Rules.
Phần tiếp theo: Tối ưu LCP — server response, ảnh hero, font, và resource hints áp dụng cụ thể.