Home / Performance / Critical Rendering Path: Hành Trình Từ HTML Đến Pixels
Performance

Critical Rendering Path: Hành Trình Từ HTML Đến Pixels

Hành trình từ HTML đến pixels: DOM, CSSOM, Render Tree, Layout, Paint và cách tối ưu Critical Rendering Path

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 chặn 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:

  1. DOM Construction: Parse HTML thành DOM (Document Object Model) tree
  2. CSSOM Construction: Parse CSS thành CSSOM (CSS Object Model) tree
  3. Render Tree: Kết hợp DOM và CSSOM tạo Render Tree (chỉ chứa nội dung hiển thị)
  4. Layout: Tính toán vị trí và kích thước của mỗi element
  5. 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 parse xong và các deferred scripts đã chạy (CSS chỉ làm chậm gián tiếp khi có blocking scripts)
  • loadEventEnd: Thời điểm tất cả resources (images, fonts) đã load xong
  • domInteractive: 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
11
12
<!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ành

Giả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
15
<!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 parse

Giả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: Nếu inline script không cần đọc computed styles, đặt nó TRƯỚC thẻ <link> CSS (không có CSS pending phía trên nên script chạy ngay). Ngoài ra, có thể chuyển script xuống cuối <body> (DOM parse xong trước khi phải đợi 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. Browser phát hiện CSS khi parse HTML

CSSOM không tự nhiên xuất hiện - nó bắt đầu được xây khi trình duyệt đọc HTML và gặp <link rel="stylesheet"> hoặc <style>:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="styles.css" />
    <!-- Tải song song, không chặn parse HTML -->
    <style>
      h1 { color: blue; } <!-- Parse ngay tại chỗ -->
    </style>
  </head>
  <body>
    <h1>Hello World</h1>
    <!-- HTML vẫn tiếp tục parse -->
  </body>
</html>

Timeline:

View Mermaid diagram code
sequenceDiagram
    participant Browser
    participant HTML
    participant CSS

    Browser->>HTML: Parse HTML
    HTML->>CSS: Gặp <link rel="stylesheet">
    Browser->>CSS: Download styles.css (song song)
    Note over Browser,HTML: HTML tiếp tục parse
    HTML->>CSS: Gặp <style> inline
    Note over CSS: Parse ngay, không cần download
    HTML-->>Browser: DOM tree hoàn thành
    CSS-->>Browser: CSSOM tree hoàn thành

Giải thích:

  • <link>: Trình duyệt tải file CSS song song, không chặn việc parse HTML (khác hẳn <script> ở bước 1)
  • <style>: Nội dung inline được parse ngay tại chỗ, không cần download
  • DOM và CSSOM được xây song song, mỗi cái từ nguồn dữ liệu riêng
  • Điểm mấu chốt: CSS không chặn parse HTML, nhưng chặn render (xem 3.3)

3.2. 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<br/>font-size: 16px"]
    Body --> H1["h1<br/>color: blue<br/>font-size: 32px (tự khai báo)"]
    Body --> P["p<br/>color: gray<br/>font-size: 16px (kế thừa từ body)"]

Giải thích:

  • CSSOM có cấu trúc cây, các thuộc tính kế thừa chảy từ cha xuống con (font-size của body truyền xuống)
  • Trừ khi con tự ghi đè: h1 khai báo font-size: 32px nên không lấy 16px từ body
  • p chỉ đặt color, nên font-size kế thừa 16px từ body
  • Nhờ vậy, khi ghép với DOM ở bước sau, mỗi element biết chính xác computed style của nó

3.3. CSS render blocking

Điểm quan trọng: CSS là render blocking - trình duyệt không render bất cứ nội dung nào cho đến khi CSSOM hoàn thành.

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="styles.css" />
    <!-- Blocking render -->
    <link rel="stylesheet" href="print.css" media="print" />
    <!-- Không chặn render trên màn hình -->
  </head>
  <body>
    <h1>Hello World</h1>
  </body>
</html>

Giải thích:

  • styles.css: Chặn render vì media mặc định áp dụng cho màn hình
  • print.css: Không chặn render màn hình vì chỉ dùng khi in (vẫn được tải, nhưng ưu tiên thấp)
  • Trình duyệt tải các file CSS song song, nhưng phải đợi parse xong CSSOM mới render

Tại sao CSS phải blocking?

1
2
3
4
5
6
<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> hiện lên rồi biến mất (flash of unstyled content - FOUC) - trải nghiệm tệ hơn việc đợi thêm 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
16
17
18
19
<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
    HTML[html] --> Body[body]
    Body --> Div[div]
    Div --> H1["h1: 'Hello'"]
    Div --> Span["span: 'Visible span'<br/>color: red"]

Giải thích:

  • <head> và toàn bộ nội dung không hiển thị bị loại khỏi Render Tree
  • <p> bị loại vì display: none (không có box để vẽ)
  • Mỗi node còn lại đi kèm computed style đã resolve từ CSSOM (ví dụ spancolor: red)
  • Render Tree vì thế là “bản đồ những gì thực sự được vẽ”, không phải toàn bộ DOM

4.2. Phân biệt display: none vs visibility: hidden

Hai thuộc tính này cùng ẩn element nhưng khác nhau ở Render Tree:

1
2
3
4
5
6
.hidden-display {
  display: none;
}
.hidden-visibility {
  visibility: hidden;
}
1
2
<div class="hidden-display">Not in Render Tree</div>
<div class="hidden-visibility">In Render Tree but invisible</div>

Giải thích:

  • display: none: Bị loại khỏi Render Tree, không tạo box, không chiếm không gian
  • visibility: hidden: Vẫn nằm trong Render Tree, vẫn tạo box và chiếm không gian - chỉ là không vẽ pixel
  • Hệ quả: bật/tắt display làm layout tính lại (reflow), còn visibility chỉ ảnh hưởng bước paint

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 dựa trên box model.

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 (giả sử box-sizing mặc định: content-box):
// Container: 800px content + 20px padding × 2 cạnh = 840px tổng chiều rộng
// Box: width 50% tính theo content width của container = 50% × 800px = 400px
// Box position: x = 20px (padding container) + 10px (margin box) = 30px; y = 30px

Giải thích:

  • Layout đi từ root xuống (top-down): cha phải có kích thước trước, con mới tính được
  • Giá trị % phải đợi cha layout xong mới resolve (như width: 50% của box)
  • Phụ thuộc viewport và kích thước cha, nên đổi kích thước cửa sổ sẽ trigger layout lại (reflow)

6. Bước 5: Paint

Paint là bước tô pixel cho từng element - background, border, text, shadow… Thứ tự tô tuân theo stacking order, trong đó z-index quyết định element nào nằm trên.

6.1. Paint order

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<style>
  .background {
    background: blue;
  }
  .text {
    position: relative;
    color: white;
    z-index: 1;
  }
  .overlay {
    position: relative;
    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 Screen

    Browser->>Browser: 1. Tô background (blue)
    Browser->>Browser: 2. Tô text (white, z-index: 1)
    Browser->>Browser: 3. Tô overlay (đen mờ, z-index: 2)
    Note over Browser: Composite các layer
    Browser->>Screen: Hiển thị kết quả cuối

Giải thích:

  • Element có z-index thấp được tô trước, cao tô sau (nằm chồng lên trên)
  • z-index chỉ có tác dụng khi element đã positioned (position khác static) hoặc là flex/grid item; nếu không, browser tô theo thứ tự xuất hiện trong document
  • Sau khi tô xong các phần, browser composite (ghép) chúng lại thành khung hình cuối

6.2. Composite layers

Một số property được xử lý thẳng ở bước composite nên animation mượt hơn hẳn:

1
2
3
4
5
6
7
8
9
10
/* Chỉ chạy ở compositor - nhanh */
.fast-animation {
  transform: translateX(100px);
  will-change: transform;
}

/* Kéo theo layout + paint - chậm */
.slow-animation {
  margin-left: 100px;
}

Giải thích:

  • transform, opacity: browser xử lý ở compositor, bỏ qua layout và paint → rất nhanh
  • margin, width, left: đổi giá trị kéo theo layout, rồi paint, rồi composite → tốn kém
  • will-change: transform gợi ý browser tách sẵn layer trước, tránh giật khi animation bắt đầu (dùng tiết chế, đừng lạm dụng)

7. Tối Ưu Critical Rendering Path

7.1. Tối ưu Critical Resources

Ba metrics quan trọng:

  1. Critical Resources: Số lượng resources chặn render
  2. Critical Bytes: Tổng dung lượng critical resources
  3. Critical Path Length: Số round trips cần thiết

Trước (3 critical resources):

1
2
3
4
5
<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>
  • Preload scanner phát hiện cả 3 resources ngay và tải chúng song song, nên vấn đề không nằm ở số round trips khi download
  • styles.css: CSS luôn render-blocking - browser phải tải và parse xong mới render được
  • analytics.jsapp.js: script thường bắt parser đứng chờ ngay tại vị trí của nó - dừng parse HTML để execute, và nếu có CSS phía trên thì còn phải đợi CSSOM xong mới chạy. Chính phần chờ tuần tự này (không phải download) mới là nút thắt

Sau (1 critical resource):

1
2
3
4
5
<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>
  • styles.css: Vẫn critical vì CSS cần thiết cho visual rendering
  • analytics.js với async: Tải song song, execute ngay khi tải xong - chỉ chặn parsing tạm thời trong lúc execute, không bắt parser đứng chờ như script thường
  • app.js với defer: Tải song song, execute sau khi HTML parsed - không block render

Kết quả cải thiện:

MetricTrướcSauCải thiện
Critical Resources31-67%
Parser đứng chờ script (tải)KhôngĐáng kể
Time to First RenderChậmNhanh 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

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
17
18
19
20
21
22
23
24
25
26
27
<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 render
  • as="style": Báo browser đây là stylesheet, giúp prioritize và cache đúng cách
  • onload="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 ScriptParse HTMLThực thi khiBlock render
<script> thườngBị chặnNgay lập tức
<script async>Tiếp tụcKhi tải xongTạm thời
<script defer>Tiếp tụcSau khi parse HTMLKhông

Giải thích chi tiết:

Để hiểu sâu hơn về asyncdefer, 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:

Bốn hint này khác nhau ở chỗ chúng làm bao nhiêu việc và cho trang nào - từ nhẹ nhất (dns-prefetch) đến nặng nhất (preload).

1. dns-prefetch

  • Chỉ làm mỗi bước phân giải tên miền (DNS lookup) sớm cho một domain, để khi thật sự request thì không phải chờ. Đây là hint nhẹ nhất, gần như không tốn tài nguyên.
  • Khi nào dùng: Third-party domains bạn chắc chắn sẽ cần nhưng chưa gấp (analytics, CDN, widget bên thứ ba).

2. preconnect

  • Đi xa hơn dns-prefetch: thiết lập trọn vẹn connection gồm DNS lookup + TCP handshake + TLS negotiation. Vì gói cả 3 bước, nó tiết kiệm được khoảng 100-500ms cho request đầu tiên tới domain đó.
  • Khi nào dùng: Third-party resources critical cần đến sớm. Connection tốn tài nguyên nên chỉ nên preconnect 2-3 domain quan trọng nhất - lạm dụng sẽ phản tác dụng.

3. preload

  • Bắt browser tải một resource cụ thể của trang hiện tại ngay với priority cao. Thuộc tính as là bắt buộc để browser đặt đúng priority và cache đúng cách:

    • as="script": JavaScript
    • as="style": CSS
    • as="image": hình ảnh
    • as="font": font (cần thêm crossorigin)
  • Khi nào dùng: Resource critical cho first render nhưng browser phát hiện muộn vì nằm sâu trong dependency - font khai báo trong CSS, ảnh trong background-image.

4. prefetch

  • Tải trước resource cho trang kế tiếp với priority thấp. Browser chỉ tải lúc rảnh nên không tranh giành băng thông với trang hiện tại.
  • Khi nào dùng: Resource của trang mà user nhiều khả năng sẽ đi tới (bước tiếp theo trong wizard, trang chi tiết từ danh sách).

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:

1. Static Import vs Dynamic Import

  • Static import (trước): mọi thứ bị gộp vào một bundle lớn - user phải tải hết ngay cả phần chưa bao giờ dùng tới.
  • Dynamic import (sau): code được tách thành các chunk nhỏ, browser chỉ tải chunk khi thật sự cần (ví dụ khi user click).

2. Lợi ích

  • Bundle ban đầu nhỏ hơn: tải và parse nhanh hơn, trang khả dụng sớm hơn
  • Time to Interactive tốt hơn: user không phải chờ những đoạn code chưa cần đến
  • Cache hiệu quả hơn: sửa một feature chỉ invalidate chunk đó, không phải cả bundle

3. Khi nào nên split

  • Theo route: mỗi trang là một chunk riêng
  • Theo component: những component nặng và ít khi mở như modal, dialog, dropdown phức tạp
  • Theo feature: phần không phải ai 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 khoảng 40-50% tổng dung lượng trang web, nên đây thường là chỗ tối ưu cho hiệu quả cao nhất. Làm tốt không chỉ tiết kiệm bandwidth mà còn kéo Largest Contentful Paint (LCP) xuống - 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 hoãn tải hình cho tới khi nó sắp lọt vào viewport - khoảng cách kích hoạt tùy tốc độ mạng (thường ~1250px trên mạng nhanh, xa hơn trên mạng chậm)
  • Ảnh above-the-fold đừng dùng loading="lazy" - việc hoãn tải sẽ làm LCP chậm đi
  • Hỗ trợ sẵn trên mọi browser hiện đại: 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
srcsetDanh sách các file ảnh kèm width thực (400w = 400 pixels wide)
sizesCho browser biết ảnh sẽ hiển thị ở size nào tùy viewport
srcFallback cho browser cũ không hỗ trợ srcset

Cách browser chọn ảnh:

  1. Đọc sizes để biết ô hiển thị ảnh rộng 400px, 800px hay 1200px
  2. Nhân với device pixel ratio (DPR) - màn Retina có DPR = 2
  3. Chọn file nhỏ nhất vẫn đủ sắc nét cho kích thước cần

Ví dụ thực tế: viewport 500px khớp (max-width: 600px) 400px → ô hiển thị 400px → DPR 2 cần 400 × 2 = 800px thực → browser chọn medium.jpg 800w

Modern Formats với Picture Element

1
2
3
4
5
6
7
8
<picture>
  <!-- AVIF - nhỏ nhất, mọi browser hiện đại đều hỗ trợ -->
  <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:

FormatGiảm size vs JPEGBrowser Support
AVIF50-60%Tất cả browser hiện đại (Chrome 85+, Firefox 93+, Safari 16.4+)
WebP25-35%Tất cả browser hiện đại
JPEGbaselineUniversal

Lưu ý: browser đọc các <source> từ trên xuống và dừng ở format đầu tiên nó hỗ trợ - chỉ tải đúng một file, không tải hết. Vì vậy hãy xếp format tốt nhất (AVIF) lên trước.

Preload LCP Image

Với hình quan trọng nhất trang (hero, banner), preload để browser tải sớm thay vì đợi phát hiện qua CSS/JS:

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 như Next.js Image hay Gatsby Image đã tích hợp sẵn LQIP và hiệu ứng blur-up, kèm luôn lazy-load và tạo srcset tự động - hầu như bạn không cần tự làm những phần trên.

8. Đo Lường Performance

Tối ưu mà không đo lường thì chỉ là đoán mò. Trước khi sửa bất cứ thứ gì, bạn cần biết chính xác bottleneck nằm ở đâu - phần nào của critical path đang kéo chậm màn hình đầu tiên. Hai công cụ dưới đây có sẵn ngay trong Chrome, không cần cài thêm gì.

8.1. Chrome DevTools Performance Tab

Chrome DevTools Performance Tab

Mở DevTools → tab Performance → nhấn Record rồi reload trang. DevTools sẽ ghi lại toàn bộ quá trình dựng trang và chia timeline thành các phase theo màu, giúp bạn nhìn ra ngay đâu là công đoạn tốn thời gian nhất:

  • Loading - màu xanh dương (tải và parse HTML)
  • Scripting - màu vàng (thực thi JavaScript)
  • Rendering - màu tím (tính Style + Layout)
  • Painting - màu xanh lá (Paint + Composite)

Nếu thanh màu vàng chiếm phần lớn timeline, JavaScript chính là thủ phạm; nếu màu tím dày đặc, bạn đang gặp vấn đề layout.

Vài mẹo khi đọc timeline:

  • Tìm các Long Task (>50ms, có gạch chéo đỏ ở góc) - đây là những tác vụ chặn main thread và làm trang đơ
  • Mở tab Bottom-Up để xem hàm nào ngốn thời gian nhất, từ đó biết cần tối ưu đoạn code nào
  • Bật Screenshots để thấy trang hiện ra từng bước - rất hữu ích khi xác định thời điểm First Paint và LCP

8.2. Lighthouse Audit

Performance tab cho bạn dữ liệu thô, còn Lighthouse thì tổng hợp lại thành điểm số và gợi ý cụ thể nên sửa gì trước. Trong DevTools, mở tab Lighthouse, chọn category Performance (và Best Practices nếu cần), rồi nhấn Analyze page load.

Report sẽ chấm điểm các Core Web Vitals (LCP, CLS, TBT) và liệt kê danh sách cải thiện kèm ước tính số mili-giây tiết kiệm được. Những gợi ý hay gặp nhất liên quan trực tiếp tới critical path:

  • Loại bỏ render-blocking resources (CSS/JS chặn hiển thị)
  • Giảm unused CSS/JS không dùng đến
  • Đặt đúng kích thước và nén hình ảnh
  • Lazy-load hình ảnh ngoài viewport
  • Minify CSS/JS

Cứ ưu tiên xử lý những mục có mức tiết kiệm lớn nhất trước, đo lại, rồi lặp - đó là cách tối ưu có định hướng thay vì sửa lung tung.

9. Best Practices Checklist

Gom lại tất cả những gì đã bàn, đây là checklist bạn có thể mở ra rà soát mỗi lần tối ưu một trang. Không cần làm hết cùng lúc - chọn nhóm nào đang là bottleneck (dựa trên số đo ở phần trên) và xử lý trước.

HTML:

  • Đặt critical CSS inline trong <head>
  • Load non-critical CSS async
  • Script với defer hoặc async
  • 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 đã đi hết hành trình từ HTML đến pixels: DOM, CSSOM, Render Tree, Layout rồi Paint. Điều quan trọng cần nhớ là mọi tối ưu Critical Rendering Path đều quy về ba con số - giảm số critical resources, giảm số bytes phải tải, và rút ngắn độ dài chuỗi phụ thuộc. Đừng cố tối ưu tất cả cùng lúc; hãy mở Chrome DevTools lên, đo trước, tìm đúng bottleneck, sửa một chỗ rồi đo lại. Cứ lặp vòng đó, trang của bạn sẽ hiện ra trước mắt người dùng nhanh hơn hẳn.

← Previous
async vs defer: Chọn Cái Nào Để Web Load Nhanh?
Table of Contents
  1. 1. Critical Rendering Path Là Gì?
    1. 1.1. Tổng quan 5 bước
    2. 1.2. Tại sao gọi là “Critical”?
  2. 2. Bước 1: Xây Dựng DOM Tree
    1. 2.1. Quá trình parse HTML
    2. 2.2. Script blocking DOM construction
    3. 2.3. Inline scripts và CSSOM dependency
  3. 3. Bước 2: Xây Dựng CSSOM Tree
    1. 3.1. Browser phát hiện CSS khi parse HTML
    2. 3.2. Quá trình parse CSS
    3. 3.3. CSS render blocking
  4. 4. Bước 3: Xây Dựng Render Tree
    1. 4.1. Từ DOM + CSSOM đến Render Tree
    2. 4.2. Phân biệt display: none vs visibility: hidden
  5. 5. Bước 4: Layout (Reflow)
  6. 6. Bước 5: Paint
    1. 6.1. Paint order
    2. 6.2. Composite layers
  7. 7. Tối Ưu Critical Rendering Path
    1. 7.1. Tối ưu Critical Resources
    2. 7.2. Inline Critical CSS
    3. 7.3. Async và Defer cho JavaScript
    4. 7.4. Resource Hints
    5. 7.5. Code Splitting
    6. 7.6. Image Optimization
  8. 8. Đo Lường Performance
    1. 8.1. Chrome DevTools Performance Tab
    2. 8.2. Lighthouse Audit
  9. 9. Best Practices Checklist