Critical Rendering Path: Hành Trình Từ HTML Đến Pixels
Bạn bao giờ tự hỏi tại sao trang web của mình load chậm, dù server response nhanh và bandwidth đủ lớn? Vấn đề có thể nằm ở Critical Rendering Path - chuỗi các bước trình duyệt phải thực hiện để chuyển HTML, CSS, JavaScript thành pixels trên màn hình.
Hiểu rõ Critical Rendering Path giúp bạn xác định chính xác bottleneck và tối ưu đúng chỗ. Thay vì đoán mò, bạn sẽ biết tại sao CSS blocking render, tại sao JavaScript làm trang trắng xóa, và cách giải quyết từng vấn đề cụ thể.
#1. Critical Rendering Path Là Gì?
Critical Rendering Path (CRP) là chuỗi các bước trình duyệt thực hiện từ khi nhận HTML cho đến khi hiển thị nội dung lên màn hình.
#1.1. Tổng quan 5 bước
View Mermaid diagram code
flowchart LR
HTML[HTML] --> DOM[1. DOM Tree]
CSS[CSS] --> CSSOM[2. CSSOM Tree]
DOM --> RenderTree[3. Render Tree]
CSSOM --> RenderTree
RenderTree --> Layout[4. Layout]
Layout --> Paint[5. Paint]
Paint --> Screen[Pixels trên màn hình]Giải thích 5 bước:
- DOM Construction: Parse HTML thành DOM (Document Object Model) tree
- CSSOM Construction: Parse CSS thành CSSOM (CSS Object Model) tree
- Render Tree: Kết hợp DOM và CSSOM tạo Render Tree (chỉ chứa nội dung hiển thị)
- Layout: Tính toán vị trí và kích thước của mỗi element
- Paint: Vẽ pixels lên màn hình
#1.2. Tại sao gọi là “Critical”?
Tất cả 5 bước này phải hoàn thành trước khi người dùng thấy nội dung đầu tiên (First Contentful Paint). Bất kỳ thứ gì làm chậm các bước này đều trực tiếp làm chậm website.
1
2
3
4
5
6
7
8
// Đo thời gian từ khi bắt đầu load đến khi render xong
window.addEventListener('load', () => {
const [navigation] = performance.getEntriesByType('navigation');
console.log('DOM ready:', navigation.domContentLoadedEventEnd, 'ms');
console.log('Page fully loaded:', navigation.loadEventEnd, 'ms');
console.log('DOM Interactive:', navigation.domInteractive, 'ms');
});Giải thích:
domContentLoadedEventEnd: Thời điểm DOM và CSSOM đã sẵn sàngloadEventEnd: Thời điểm tất cả resources (images, fonts) đã load xongdomInteractive: Thời điểm DOM parse xong, trước khi chạy deferred scripts- Critical Rendering Path ảnh hưởng trực tiếp đến các metrics này
#2. Bước 1: Xây Dựng DOM Tree
Trình duyệt đọc HTML từ trên xuống và chuyển thành DOM tree.
#2.1. Quá trình parse HTML
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
</head>
<body>
<h1>Hello World</h1>
<p>This is a paragraph</p>
</body>
</html>DOM Tree được tạo ra:
View Mermaid diagram code
flowchart TD
Document[Document] --> HTML[html]
HTML --> Head[head]
HTML --> Body[body]
Head --> Title[title]
Title --> TitleText["'My Website'"]
Body --> H1[h1]
Body --> P[p]
H1 --> H1Text["'Hello World'"]
P --> PText["'This is a paragraph'"]Giải thích:
- Mỗi HTML tag trở thành một node trong DOM tree
- Text content cũng là node
- Parse là incremental - trình duyệt không đợi tải hết HTML mới bắt đầu
#2.2. Script blocking DOM construction
Khi gặp thẻ <script>, trình duyệt phải dừng parse HTML:
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
<script src="app.js"></script> <!-- Parse HTML dừng tại đây -->
</head>
<body>
<h1>Hello World</h1> <!-- Chưa được parse -->
</body>
</html>Timeline:
View Mermaid diagram code
sequenceDiagram
participant Browser
participant HTML
participant Script
Browser->>HTML: Parse HTML
HTML->>Script: Gặp script tag
Note over Browser: Dừng parse HTML
Browser->>Script: Tải và chạy app.js
Note over Browser: HTML bị chặn
Script-->>Browser: Script chạy xong
Browser->>HTML: Tiếp tục parse
HTML-->>Browser: DOM tree hoàn thànhGiải thích:
Đây chính là lý do tại sao script làm chậm rendering. Trong thực tế, có thể khắc phục bằng thuộc tính async hoặc defer (sẽ đề cập ở phần tối ưu).
#2.3. Inline scripts và CSSOM dependency
Inline scripts có behavior đặc biệt - chúng phải đợi CSSOM hoàn thành nếu có CSS phía trên:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="styles.css"> <!-- CSS blocking -->
<script>
// Script này phải đợi styles.css parse xong!
const color = getComputedStyle(document.body).color;
console.log(color);
</script>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>Timeline:
View Mermaid diagram code
sequenceDiagram
participant Browser
participant HTML
participant CSS
participant Script
Browser->>HTML: Parse HTML
HTML->>CSS: Gặp stylesheet
Browser->>CSS: Download CSS
HTML->>Script: Gặp inline script
Note over Browser: Dừng parse HTML
Note over Browser: Đợi CSS download + parse
CSS-->>Browser: CSSOM ready
Browser->>Script: Execute inline script
Script-->>Browser: Script xong
Browser->>HTML: Tiếp tục parseGiải thích:
- Inline scripts không thể dùng
async/defer- luôn blocking - Browser giả định script có thể query styles (như
getComputedStyle) - Do đó phải đợi tất cả CSS phía trên parse xong mới chạy script
- Đây là “hidden cost” của inline scripts mà nhiều dev không biết
Best practice: Đặt inline scripts sau CSS hoặc chuyển sang external với defer.
#3. Bước 2: Xây Dựng CSSOM Tree
Tương tự DOM, CSS được parse thành CSSOM tree.
#3.1. Quá trình parse CSS
1
2
3
4
5
6
7
8
9
10
11
12
body {
font-size: 16px;
}
h1 {
color: blue;
font-size: 32px;
}
p {
color: gray;
}CSSOM Tree:
View Mermaid diagram code
flowchart TD
Root[CSSOM Root] --> Body[body]
Body --> BodyRules["font-size: 16px"]
Root --> H1[h1]
H1 --> H1Rules["color: blue<br/>font-size: 32px<br/>(inherits: font-size từ body)"]
Root --> P[p]
P --> PRules["color: gray<br/>(inherits: font-size từ body)"]Giải thích:
- CSSOM tree chứa tất cả computed styles
- Bao gồm cả inheritance (kế thừa) từ parent elements
- Mỗi node biết chính xác style nào áp dụng cho nó
#3.2. CSS render blocking
Điểm quan trọng: CSS là render blocking - trình duyệt không thể render cho đến khi CSSOM hoàn thành.
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="styles.css"> <!-- Blocking -->
<link rel="stylesheet" href="print.css" media="print"> <!-- Non-blocking for screen -->
</head>
<body>
<h1>Hello World</h1>
</body>
</html>Giải thích:
styles.css: Blocking vì áp dụng cho màn hìnhprint.css: Non-blocking vì chỉ áp dụng khi print- Trình duyệt download CSS song song nhưng vẫn đợi parse xong mới render
#Tại sao CSS phải blocking?
1
2
3
4
<style>
h1 { display: none; }
</style>
<h1>This heading should be hidden</h1>Nếu trình duyệt render trước khi CSSOM sẵn sàng, người dùng sẽ thấy <h1> nhấp nháy (flash) rồi biến mất - trải nghiệm tệ hơn việc đợi một chút.
#4. Bước 3: Xây Dựng Render Tree
Render Tree kết hợp DOM và CSSOM, chỉ chứa những gì sẽ hiển thị.
#4.1. Từ DOM + CSSOM đến Render Tree
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<head>
<style>
p { display: none; }
span { color: red; }
</style>
</head>
<body>
<div>
<h1>Hello</h1>
<p>Hidden paragraph</p>
<span>Visible span</span>
</div>
</body>
</html>Render Tree chỉ chứa:
View Mermaid diagram code
flowchart TD
Root[Render Tree Root] --> Div[div]
Div --> H1[h1: 'Hello']
Div --> Span["span: 'Visible span'<br/>color: red"]Giải thích:
<p>không có trong Render Tree vìdisplay: none<head>và nội dung không hiển thị cũng bị loại bỏ- Chỉ element hiển thị kèm computed styles được giữ lại
#
1
2
.hidden-display { display: none; }
.hidden-visibility { visibility: hidden; }Giải thích:
display: none: Không có trong Render Tree, không chiếm không gianvisibility: hidden: Có trong Render Tree, vẫn chiếm không gian (invisible box)
1
2
<div class="hidden-display">Not in Render Tree</div>
<div class="hidden-visibility">In Render Tree but invisible</div>#5. Bước 4: Layout (Reflow)
Layout tính toán vị trí và kích thước chính xác của mỗi element.
#5.1. Box Model và Layout
1
2
3
4
5
6
7
8
9
10
11
12
13
<style>
.container {
width: 800px;
padding: 20px;
}
.box {
width: 50%;
margin: 10px;
}
</style>
<div class="container">
<div class="box">Content</div>
</div>Layout calculation:
1
2
3
4
// Trình duyệt tính toán:
// Container: 800px width + 20px padding = 840px total
// Box: 50% of 800px = 400px width
// Box position: x=30px (20px padding + 10px margin), y=30pxGiải thích:
- Layout đi từ root xuống (top-down)
- Phụ thuộc vào viewport size, parent dimensions
- % values phải đợi parent layout xong mới tính được
#5.2. Layout thrashing
Vấn đề performance nghiêm trọng:
1
2
3
4
5
6
7
8
// Sai: Forced synchronous layout
const boxes = document.querySelectorAll('.box');
boxes.forEach(box => {
const height = box.offsetHeight; // Đọc - trigger layout
box.style.height = height + 10 + 'px'; // Ghi - invalidate layout
// Mỗi iteration trigger layout lại!
});Tối ưu:
1
2
3
4
5
6
7
8
9
10
// Đúng: Batch đọc và ghi riêng biệt
const boxes = document.querySelectorAll('.box');
// Đọc tất cả trước
const heights = Array.from(boxes).map(box => box.offsetHeight);
// Ghi tất cả sau
boxes.forEach((box, index) => {
box.style.height = heights[index] + 10 + 'px';
});Giải thích:
- Đọc properties như
offsetHeight,clientWidthtrigger layout - Ghi styles invalidate layout
- Xen kẽ đọc/ghi gây nhiều lần layout không cần thiết (layout thrashing)
#6. Bước 5: Paint
Paint vẽ pixels lên màn hình theo thứ tự z-index.
#6.1. Paint order
1
2
3
4
5
6
7
8
9
<style>
.background { background: blue; }
.text { color: white; z-index: 1; }
.overlay { background: rgba(0,0,0,0.5); z-index: 2; }
</style>
<div class="background">
<p class="text">Text content</p>
<div class="overlay">Overlay</div>
</div>Thứ tự paint:
View Mermaid diagram code
sequenceDiagram
participant Browser
participant Layer
Browser->>Layer: 1. Paint background (blue)
Browser->>Layer: 2. Paint text (white, z-index: 1)
Browser->>Layer: 3. Paint overlay (semi-transparent, z-index: 2)
Layer-->>Browser: Composite layers
Browser->>Screen: Display final resultGiải thích:
- Paint theo thứ tự z-index (thấp đến cao)
- Mỗi layer được paint riêng
- Cuối cùng composite (ghép) các layers lại
#6.2. Composite layers
Một số properties tạo composite layer riêng, render nhanh hơn:
1
2
3
4
5
6
7
8
9
10
/* Tạo composite layer - nhanh */
.fast-animation {
transform: translateX(100px);
will-change: transform;
}
/* Không tạo layer - chậm */
.slow-animation {
margin-left: 100px; /* Trigger layout + paint */
}Giải thích:
transform,opacity: Chỉ affect composite, không trigger layout/paintmargin,width,left: Trigger layout, rồi paint, rồi composite (chậm)will-changehint cho browser tạo layer trước
#7. Tối Ưu Critical Rendering Path
#7.1. Tối ưu Critical Resources
Ba metrics quan trọng:
- Critical Resources: Số lượng resources chặn render
- Critical Bytes: Tổng dung lượng critical resources
- Critical Path Length: Số round trips cần thiết
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- Trước: 3 critical resources, 3 round trips -->
<head>
<link rel="stylesheet" href="styles.css"> <!-- Critical -->
<script src="analytics.js"></script> <!-- Critical nhưng không cần thiết -->
<script src="app.js"></script> <!-- Critical -->
</head>
<!-- Sau: 1 critical resource, 1 round trip -->
<head>
<link rel="stylesheet" href="styles.css"> <!-- Critical -->
<script async src="analytics.js"></script> <!-- Non-blocking -->
<script defer src="app.js"></script> <!-- Non-blocking -->
</head>Giải thích:
Trước (3 critical resources):
styles.css: CSS luôn block render - browser phải đợi tải xong mới renderanalytics.js: Script thông thường block HTML parsing và renderapp.js: Script thông thường cũng block - browser tải và execute tuần tự
Sau (1 critical resource):
styles.css: Vẫn critical vì CSS cần thiết cho visual renderinganalytics.jsvớiasync: Tải song song, execute ngay khi ready - không block renderapp.jsvớidefer: Tải song song, execute sau khi HTML parsed - không block render
Kết quả cải thiện:
| Metric | Trước | Sau | Cải thiện |
|---|---|---|---|
| Critical Resources | 3 | 1 | -67% |
| Critical Path Length | 3 round trips | 1 round trip | -67% |
| Time to First Render | Chậm | Nhanh hơn | Đáng kể |
Chỉ còn CSS là render-blocking, JavaScript được tải và thực thi mà không làm chậm việc hiển thị trang.
#7.2. Inline Critical CSS

Above-the-fold là phần nội dung user nhìn thấy ngay khi trang load, không cần scroll. Đây là phần cần Critical CSS.
Below-the-fold là phần nội dung nằm dưới màn hình, user phải scroll mới thấy. CSS cho phần này có thể tải sau.
Chiến lược tối ưu:
- Above-the-fold: Inline Critical CSS → build CSSOM ngay khi parse HTML, không đợi network
- Below-the-fold: Load CSS async → không block first paint
Cách triển khai:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<head>
<!-- Critical CSS inline để render ngay lập tức -->
<style>
/* Styles cho phần above-the-fold */
.header { background: #333; color: white; }
.main-content { max-width: 1200px; margin: 0 auto; }
</style>
<!-- Preload CSS không critical -->
<link rel="preload" href="css/styles.css" as="style" onload="this.rel='stylesheet'">
<!-- Fallback cho users không có JavaScript -->
<noscript>
<link rel="stylesheet" href="css/styles.css">
</noscript>
</head>Giải thích:
#Inline Critical CSS
Browser có thể build CSSOM ngay khi parse HTML vì CSS đã nằm sẵn trong document, không cần đợi network request. Điều này giúp Render Tree được tạo sớm hơn.
#Async CSS Loading với Preload
rel="preload": Tải CSS với priority cao nhưng không block renderas="style": Báo browser đây là stylesheet, giúp prioritize và cache đúng cáchonload="this.rel='stylesheet'": Khi load xong, chuyển thành stylesheet để apply styles
#Fallback cho No-JavaScript
<noscript> đảm bảo users không có JavaScript vẫn nhận được CSS qua thẻ <link> thông thường.
Kết quả: Page render nhanh với critical CSS, full styles load và apply sau mà không làm chậm First Contentful Paint (FCP).
#7.3. Async và Defer cho JavaScript
1
2
3
4
5
6
7
<head>
<!-- Script chính của app: dùng defer -->
<script defer src="app.js"></script>
<!-- Script bên thứ 3: dùng async -->
<script async src="https://www.google-analytics.com/analytics.js"></script>
</head>So sánh blocking behavior:
| Loại Script | Parse HTML | Thực thi khi | Block render |
|---|---|---|---|
<script> thường | Bị chặn | Ngay lập tức | Có |
<script async> | Tiếp tục | Khi tải xong | Tạm thời |
<script defer> | Tiếp tục | Sau khi parse HTML | Không |
Giải thích chi tiết:
Để hiểu sâu hơn về async và defer, xem bài viết async vs defer: Chọn Cái Nào Để Web Load Nhanh?.
#7.4. Resource Hints
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<head>
<!-- DNS prefetch: Resolve domain trước -->
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<!-- Preconnect: Thiết lập connection sớm -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<!-- Preload: Tải resource với priority cao -->
<link rel="preload" href="critical.js" as="script">
<link rel="preload" href="hero-image.jpg" as="image">
<!-- Prefetch: Tải trước cho trang kế tiếp -->
<link rel="prefetch" href="next-page.js">
</head>Giải thích:
#dns-prefetch
Resolve domain name trước khi cần. Browser thực hiện DNS lookup sớm, giảm latency khi thực sự request resource từ domain đó.
Khi nào dùng: Third-party domains mà bạn biết chắc sẽ cần (fonts, analytics, CDN).
#preconnect
Thiết lập full connection trước: DNS lookup + TCP handshake + TLS negotiation. Tiết kiệm được 100-500ms so với connection lần đầu.
Khi nào dùng: Critical third-party resources cần load sớm. Chỉ dùng cho 2-3 domains quan trọng nhất vì connection tốn tài nguyên.
#preload
Báo browser tải resource ngay với priority cao. Khác với prefetch, preload dùng cho trang hiện tại và được ưu tiên cao.
as="script": Báo đây là JavaScriptas="style": Báo đây là CSSas="image": Báo đây là hình ảnhas="font": Báo đây là font (cần thêmcrossorigin)
Khi nào dùng: Critical resources cần cho first render nhưng browser chưa discover sớm (fonts trong CSS, images trong CSS background).
#prefetch
Tải trước resources cho trang kế tiếp với low priority. Browser tải khi idle, không ảnh hưởng trang hiện tại.
Khi nào dùng: Resources của trang mà user có khả năng cao sẽ navigate đến (next page trong wizard, product detail từ listing).
#7.5. Code Splitting
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Trước: Load tất cả ngay từ đầu
import { featureA } from './features';
import { featureB } from './features';
import { featureC } from './features';
// Sau: Dynamic import khi cần
const loadFeatureA = () => import('./featureA');
const loadFeatureB = () => import('./featureB');
// Chỉ load khi user click
button.addEventListener('click', async () => {
const module = await loadFeatureA();
module.initialize();
});Giải thích:
#Static Import vs Dynamic Import
Static import (trước): Tất cả code được bundle vào một file lớn. User phải tải toàn bộ dù chưa cần dùng.
Dynamic import (sau): Code được chia thành chunks nhỏ. Browser chỉ tải chunk khi thực sự cần.
#Lợi ích
- Giảm bundle size ban đầu: Main bundle nhỏ hơn, tải nhanh hơn
- Faster Time to Interactive: User tương tác được sớm hơn vì không phải đợi load code chưa cần
- Better caching: Thay đổi một feature không invalidate toàn bộ bundle
#Khi nào nên split
- Route-based splitting: Mỗi trang là một chunk riêng
- Component-based splitting: Modal, dialog, dropdown phức tạp
- Feature-based splitting: Features không phải user nào cũng dùng (admin panel, export PDF)
1
2
3
// Route-based splitting với React
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Settings = React.lazy(() => import('./pages/Settings'));#7.6. Image Optimization
Hình ảnh thường chiếm 50-70% tổng dung lượng trang web. Tối ưu hình ảnh không chỉ giảm bandwidth mà còn cải thiện Largest Contentful Paint (LCP) - một trong những Core Web Vitals quan trọng nhất.
#Native Lazy Loading
1
2
<!-- Native lazy loading - không cần JavaScript -->
<img src="image.jpg" loading="lazy" alt="Mô tả">Cách hoạt động:
- Browser chỉ tải hình khi nó sắp xuất hiện trong viewport (thường ~1250px trước khi scroll tới)
- Hình ảnh above-the-fold (đầu trang) không nên dùng
loading="lazy"- sẽ delay LCP - Browser hỗ trợ: Chrome 77+, Firefox 75+, Safari 15.4+ (2022)
1
2
3
4
5
6
<!-- Hero image - KHÔNG lazy load -->
<img src="hero.jpg" alt="Banner chính" fetchpriority="high">
<!-- Hình dưới fold - lazy load -->
<img src="product-1.jpg" loading="lazy" alt="Sản phẩm 1">
<img src="product-2.jpg" loading="lazy" alt="Sản phẩm 2">#Responsive Images với srcset/sizes
1
2
3
4
5
6
<img
srcset="small.jpg 400w, medium.jpg 800w, large.jpg 1200w"
sizes="(max-width: 600px) 400px, (max-width: 900px) 800px, 1200px"
src="large.jpg"
alt="Mô tả"
>Giải thích từng phần:
| Attribute | Ý nghĩa |
|---|---|
srcset | Danh sách các file ảnh kèm width thực (400w = 400 pixels wide) |
sizes | Cho browser biết ảnh sẽ hiển thị ở size nào tùy viewport |
src | Fallback cho browser cũ không hỗ trợ srcset |
Cách browser chọn ảnh:
- Browser đọc
sizesđể biết ảnh sẽ hiển thị 400px, 800px hay 1200px - Kết hợp với device pixel ratio (DPR) - Retina display có DPR = 2
- Chọn file nhỏ nhất đáp ứng được yêu cầu
Ví dụ thực tế: Màn hình 500px với DPR 2 → cần ảnh 1000px thực → browser chọn large.jpg 1200w
#Modern Formats với Picture Element
1
2
3
4
5
6
7
8
<picture>
<!-- AVIF - nhỏ nhất, hỗ trợ mới -->
<source srcset="image.avif" type="image/avif">
<!-- WebP - cân bằng giữa size và support -->
<source srcset="image.webp" type="image/webp">
<!-- JPEG - fallback universal -->
<img src="image.jpg" alt="Mô tả">
</picture>So sánh format:
| Format | Giảm size vs JPEG | Browser Support |
|---|---|---|
| AVIF | 50-60% | Chrome 85+, Firefox 93+ |
| WebP | 25-35% | Tất cả browser hiện đại |
| JPEG | baseline | Universal |
Lưu ý: <picture> element cho phép browser chọn format tốt nhất mà nó hỗ trợ - không tải tất cả các source.
#Preload LCP Image
Với hình ảnh quan trọng nhất (hero image, banner), dùng preload để browser tải sớm:
1
2
3
4
5
6
7
8
9
10
<head>
<!-- Preload hero image với responsive -->
<link
rel="preload"
as="image"
href="hero.webp"
imagesrcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w"
imagesizes="100vw"
>
</head>Khi nào dùng preload image:
- Hero image, banner chính
- Background image trong CSS (browser phát hiện muộn)
- LCP element là hình ảnh
#Placeholder Strategies
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- Dominant color placeholder -->
<img
src="product.jpg"
loading="lazy"
style="background-color: #e8d5b7;"
alt="Sản phẩm"
>
<!-- Low-quality image placeholder (LQIP) -->
<img
src="product-tiny.jpg"
data-src="product-full.jpg"
class="lazyload blur-up"
alt="Sản phẩm"
>Trong thực tế, các framework hiện đại như Next.js Image, Gatsby Image đã tích hợp sẵn LQIP và blur-up effect - bạn không cần implement thủ công.
#8. Đo Lường Performance
#8.1. Chrome DevTools Performance Tab

- Mở DevTools → Performance tab → Record → Reload page
- Xem các phases trong timeline:
- Parsing: Màu xanh dương (HTML parsing)
- Scripting: Màu vàng (JavaScript execution)
- Rendering: Màu tím (Style + Layout)
- Painting: Màu xanh lá (Paint + Composite)
Tips:
- Tìm “Long Tasks” (>50ms) - đây là bottleneck
- Check “Bottom-Up” tab để xem function nào tốn thời gian nhất
- Enable “Screenshots” để thấy visual progress
#8.2. Lighthouse Audit
Trong Chrome DevTools:
- DevTools → Lighthouse tab
- Chọn categories: Performance, Best Practices
- Generate report
Opportunities section sẽ gợi ý:
- Loại bỏ render-blocking resources
- Giảm unused CSS
- Resize hình ảnh đúng kích thước
- Defer hình ảnh ngoài viewport
- Minify CSS/JS
#9. Best Practices Checklist
HTML:
- ✅ Đặt critical CSS inline trong
<head> - ✅ Load non-critical CSS async
- ✅ Script với
deferhoặcasync - ✅ Preload critical resources
CSS:
- ✅ Minify và compress CSS
- ✅ Remove unused CSS
- ✅ Use media queries để conditional load
- ✅ Avoid @import (blocking)
JavaScript:
- ✅ Code splitting cho large apps
- ✅ Tree shaking để remove dead code
- ✅ Lazy load features không cần ngay
- ✅ Avoid layout thrashing
Images:
- ✅ Lazy load offscreen images
- ✅ Serve responsive images với srcset
- ✅ Use modern formats (WebP, AVIF)
- ✅ Optimize và compress images
Fonts:
- ✅ Preload critical fonts
- ✅ Use
font-display: swap - ✅ Subset fonts (chỉ characters cần dùng)
- ✅ Self-host fonts thay vì CDN
Vậy là bạn đã hiểu toàn bộ Critical Rendering Path từ HTML đến pixels trên màn hình. Trong thực tế, hãy tập trung vào ba metric: giảm critical resources, giảm critical bytes, và rút ngắn critical path length. Dùng Chrome DevTools để đo lường, xác định bottleneck, và tối ưu từng bước một.