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:

  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 và CSSOM đã sẵn sàng
  • 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
<!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
<!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: Đặ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ình
  • print.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

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

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 gian
  • visibility: 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=30px

Giả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, clientWidth trigger 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 result

Giả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/paint
  • margin, width, left: Trigger layout, rồi paint, rồi composite (chậm)
  • will-change hint 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:

  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
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 render
  • analytics.js: Script thông thường block HTML parsing và render
  • app.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 rendering
  • analytics.js với async: Tải song song, execute ngay khi ready - không block render
  • 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%
Critical Path Length3 round trips1 round trip-67%
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
<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:

#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à JavaScript
  • as="style": Báo đây là CSS
  • as="image": Báo đây là hình ảnh
  • as="font": Báo đây là font (cần thêm crossorigin)

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
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. Browser đọc sizes để biết ảnh sẽ hiển thị 400px, 800px hay 1200px
  2. Kết hợp với device pixel ratio (DPR) - Retina display có DPR = 2
  3. 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:

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

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

hrome DevTools Performance Tab

  1. Mở DevTools → Performance tab → Record → Reload page
  2. 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:

  1. DevTools → Lighthouse tab
  2. Chọn categories: Performance, Best Practices
  3. 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 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 đã 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.