Skip to content

Pipeline Kết Xuất Trình Duyệt

🎯 Câu Hỏi Cốt Lõi

Tại sao một số trang web mượt như lụa, trong khi số khác lại giật như trình chiếu PPT? Trình duyệt biến đống code HTML, CSS, JavaScript thành trang web bạn nhìn thấy như thế nào? Chương này sẽ đưa bạn vào sâu bên trong "xưởng sản xuất" của trình duyệt, hiểu quy trình làm việc của nó, từ đó viết ra những trang web hiệu năng tốt hơn.

Bài viết này sẽ dạy bạn điều gì?

ChươngNội DungHọc Xong Có Thể Làm Gì
Chương 1Tại sao cần hiểu pipeline kết xuấtHiểu sự cần thiết của tối ưu hiệu năng
Chương 2Năm giai đoạn của pipeline kết xuấtNắm vững quy trình kết xuất cơ bản của trình duyệt
Chương 3Xây dựng cây DOM và cây CSSOMHiểu HTML và CSS được phân tích như thế nào
Chương 4Xây dựng cây kết xuấtBiết những phần tử nào sẽ được kết xuất
Chương 5Layout và ReflowTránh kích hoạt tính toán layout đắt đỏ
Chương 6Paint và RepaintGiảm thao tác vẽ không cần thiết
Chương 7Composite và tăng tốc GPUTận dụng GPU để nâng cao hiệu năng animation
Chương 8Event LoopHiểu cơ chế thực thi của JavaScript
Chương 9Thực chiến tối ưu hiệu năngNắm vững các kỹ thuật tối ưu hiệu năng phổ biến

Mỗi chương đều bắt đầu từ "hiểu nguyên lý", không cần bạn phải tự tay viết code tối ưu. Khi gặp vấn đề hiệu năng, quay lại tra cứu bất cứ lúc nào.


1. Tại Sao Cần Hiểu "Pipeline Kết Xuất"?

1.1 Từ "Chạy Được" Đến "Chạy Nhanh": Con Đường Tiến Bộ Của Frontend

Khi mới học frontend, chúng ta chỉ quan tâm code "có chạy được không" — trang hiển thị được, nút bấm được, coi như thành công. Nhưng khi dự án lớn dần, người dùng đông dần, bạn sẽ nhanh chóng phát hiện một thực tế tàn khốc: cùng một chức năng, có người viết trang mượt như lụa, có người viết lại giật đến mức người dùng muốn đập chuột.

Điều này giống như học lái xe. Người mới chỉ quan tâm "xe có chạy được không", nhưng tài xế lão luyện sẽ quan tâm "khi nào nên sang số, khi nào nên phanh, lái thế nào tiết kiệm xăng nhất". Trình duyệt chính là chiếc "xe" bạn đang lái, hiểu "thói quen làm việc" của nó, bạn mới có thể lái nhanh và ổn định.

🐢 Tư Duy Người Mới (chỉ quan tâm chức năng)

  • Chỉ cần trang hiển thị được là được
  • Giật lag là lỗi của trình duyệt
  • Tối ưu hiệu năng là việc để sau

🚀 Tư Duy Nâng Cao (quan tâm trải nghiệm)

  • Độ mượt là cốt lõi của trải nghiệm người dùng
  • Hiểu quy trình làm việc của trình duyệt
  • Viết code đã nghĩ đến hiệu năng

Hiểu pipeline kết xuất, chính là bước then chốt từ "chạy được" đến "chạy nhanh".

1.2 Một Câu Chuyện Thực Tế: Tại Sao "Tối Ưu" Rồi Lại Càng Chậm Hơn?

Nhật Ký Vấp Ngã Về Hiệu Năng Của Tiểu Trương

Tiểu Trương là frontend engineer của một công ty thương mại điện tử, phụ trách tối ưu trang chi tiết sản phẩm. Trang này khi hiển thị thông tin sản phẩm giật kinh khủng, người dùng phàn nàn liên tục.

Tiểu Trương nghĩ: "Trang giật chắc là do DOM quá nhiều, em dùng display:none ẩn trước, sửa xong rồi hiển thị, như vậy trình duyệt sẽ không kết xuất lặp lại chứ?"

Thế là cậu ấy viết code như sau:

javascript
// Bạn nghĩ đây là "tối ưu"
const container = document.getElementById('list')
container.style.display = 'none'  // Ẩn trước, chắc sẽ không kích hoạt kết xuất?

for (let i = 0; i < 1000; i++) {
  const item = document.createElement('div')
  item.style.width = Math.random() * 100 + 'px'  // Độ rộng ngẫu nhiên
  container.appendChild(item)
}

container.style.display = 'block'  // Cuối cùng hiển thị, kết xuất một lần

Kết quả test phát hiện, trang càng giật hơn! Tiểu Trương ngơ ngác: rõ ràng đã "tối ưu" rồi, tại sao lại còn chậm hơn?

Sau đó tech lead xem code, chỉ ra vấn đề: mặc dù phần tử bị ẩn, nhưng mỗi lần bạn sửa style.width vẫn kích hoạt tính toán style và đánh dấu layout của trình duyệt, trình duyệt đã làm rất nhiều việc vô ích ở background.

Cách đúng là dùng DocumentFragment thao tác hàng loạt trong bộ nhớ, cuối cùng chèn một lần vào DOM, chỉ kích hoạt kết xuất một lần.

💡 Bài Học Cốt Lõi

Không hiểu quy trình làm việc của trình duyệt, bạn có thể "tự cho là thông minh" viết ra một đống "code tối ưu", kết quả lại làm hiệu năng tệ hơn. Hiểu pipeline kết xuất, bạn mới biết thao tác nào đắt đỏ, thao tác nào rẻ, từ đó tránh dùng sức sai chỗ.


2. Khái Niệm Cốt Lõi: "Pipeline Kết Xuất" Là Gì?

🤔 "Kết Xuất" Là Gì?

Kết xuất (Rendering), nói đơn giản là quá trình trình duyệt "vẽ" code thành trang web bạn nhìn thấy.

Bạn có thể tưởng tượng nó như nhà in sách:

  • HTML = nội dung bản thảo (chữ, hình ảnh, chương mục)
  • CSS = yêu cầu dàn trang (cỡ chữ, màu sắc, khoảng cách)
  • JavaScript = chỉnh sửa động (tác giả sửa bản thảo tạm thời, điều chỉnh dàn trang)

Trình duyệt nhận những "nguyên liệu" này, phải qua từng "công đoạn", cuối cùng mới "in" ra trang web bạn nhìn thấy. Chuỗi công đoạn này, chính là pipeline kết xuất (Rendering Pipeline).

Để giúp bạn hiểu rõ hơn, chúng ta dùng một tiệm bánh để so sánh với quy trình kết xuất của trình duyệt.

2.1 Dùng Tiệm Bánh Để Hiểu Pipeline Kết Xuất

Hãy tưởng tượng bạn đang vận hành một tiệm bánh, mỗi ngày phải làm các loại bánh cho khách hàng. Các công đoạn trong quá trình này, giống một cách đáng kinh ngạc với quy trình kết xuất của trình duyệt:

Giai Đoạn🥖 So Sánh Tiệm BánhCông Việc Thực Tế Của Trình DuyệtVí Dụ Cụ Thể
1. Chuẩn bị nguyên liệuSắp xếp danh sách nguyên liệu (bột mì, trứng, kem...)Xây dựng cây DOM: phân tích HTML thành cấu trúc câyBạn viết <div><p>Hello</p></div>, trình duyệt phân tích thành cây div→p→"Hello"
2. Chuẩn bị công thứcSắp xếp thẻ công thức (tỉ lệ nguyên liệu mỗi loại bánh)Xây dựng cây CSSOM: phân tích CSS thành cây quy tắcBạn viết .title { color: red }, trình duyệt ghi nhận "chữ của .title là màu đỏ"
3. Lập kế hoạchDựa vào nguyên liệu và công thức, quyết định hôm nay làm bánh gìXây dựng cây kết xuất: hợp nhất DOM và CSSOM, chỉ giữ phần tử hiển thịThẻ <script> không hiển thị, nên không có trong cây kết xuất
4. Sắp xếp vị tríBày bánh vào tủ trưng bày, quyết định mỗi bánh đặt ở đâuLayout: tính toán kích thước và vị trí mỗi phần tửTính ra "div này rộng 200px, cao 100px, ở vị trí (50, 50) trên màn hình"
5. Tô màu trang tríQuét trứng, rắc mè, bóp kem lên bánhPaint: "vẽ" màu sắc, viền, bóng của phần tửThực sự vẽ "chữ màu đỏ" lên màn hình
6. Lắp ráp hoàn thànhXếp chồng tất cả bánh lại với nhau, bày thành hình đẹpComposite: hợp nhất nhiều layer thành hình ảnh cuối cùngGPU hợp nhất layer nền, layer chữ, layer ảnh thành một bức tranh hoàn chỉnh

📊 Bạn Có Thể Thấy Gì Từ Bảng Này?

Hãy đọc từng dòng của bảng này, hiểu từng giai đoạn của pipeline kết xuất:

Giai đoạn 1-2 (giai đoạn chuẩn bị): Trình duyệt trước tiên "hiểu" code của bạn. HTML và CSS được phân tích riêng biệt, vì chúng có trách nhiệm khác nhau — HTML quyết định "có nội dung gì", CSS quyết định "trông như thế nào".

Giai đoạn 3 (giai đoạn hợp nhất): Tại sao cần "hợp nhất"? Vì không phải tất cả phần tử HTML đều hiển thị (như <head>, <script>), trình duyệt cần kết hợp "phần tử hiển thị" và "style của chúng" lại với nhau, tạo thành một "bản vẽ thi công".

Giai đoạn 4-5 (giai đoạn vẽ): Layout là "tính vị trí", Paint là "tô màu". Thay đổi layout (như đổi độ rộng) sẽ dẫn đến paint, nhưng thay đổi paint (như đổi màu) không dẫn đến layout.

Giai đoạn 6 (giai đoạn composite): "Phép màu" của trình duyệt hiện đại. Cách truyền thống là "vẽ xong một lần" (CPU chậm), cách hiện đại là "vẽ theo layer + GPU composite" (nhanh), đây chính là lý do animation transform mượt hơn animation width.

2.2 Năm Giai Đoạn Của Pipeline Kết Xuất

🏭渲染管线从代码到像素的五步旅程
想象你在印刷厂工作:稿件要排版、印刷、装订,最后才能变成书本。浏览器渲染网页也一样,HTML 和 CSS 要经过一道道"工序",才能变成屏幕上的画面。
🌲
构建DOM/CSSOM
解析代码
🎨
构建渲染树
合并筛选
📐
布局
计算位置
✏️
绘制
填充颜色
🔮
合成
合并图层
👆 点击上方任意阶段,查看详细解释
💡核心思想:每个阶段各司其职,前面的阶段为后面阶段准备数据。理解这个流程,你就能知道什么时候用什么方式修改页面,才能避免性能问题。

3. Giai Đoạn 1: Xây Dựng Cây DOM và Cây CSSOM

3.1 Tại Sao Cần "Cây Hóa"?

🤔 DOM Là Gì?

DOM (Document Object Model), là cấu trúc cây mà trình duyệt chuyển đổi từ tài liệu HTML, thuận tiện cho JavaScript thao tác các phần tử trang.

Bạn có thể tưởng tượng nó như cây gia phả:

  • Đỉnh trên cùng là "tổ tiên" (<html>)
  • Bên dưới là "con cháu" (<body>, <head>)
  • Bên dưới nữa là "cháu chắt" (<div>, <p>, <span>)

Tại sao phải chuyển thành cây? Vì cấu trúc cây rất tiện cho việc "tìm kiếm" và "sửa đổi". Ví dụ bạn muốn tìm "tất cả phần tử có class là title", trình duyệt có thể tìm kiếm nhanh trên cây, thay vì phải tìm từ từ trong đống văn bản lộn xộn.

Trình duyệt nhận HTML xong, không hiển thị ngay, mà phải "hiểu" nó trước. Quá trình này chia làm ba bước:

Bước 1: Phân tích từ vựng — tách code thành "từ"

html
<div class="container">
  <p>Hello World</p>
</div>

Trình duyệt thấy đoạn code này, sẽ "tách từ" trước:

  • <div> → "thẻ mở div"
  • class="container" → "thuộc tính class, giá trị container"
  • <p> → "thẻ mở p"
  • Hello World → "nội dung văn bản"
  • </p> → "thẻ đóng p"
  • </div> → "thẻ đóng div"

Bước 2: Phân tích cú pháp — lắp ráp "từ" thành "nút"

Trình duyệt dựa vào quy tắc HTML, lắp ráp những "từ" này thành "nút":

  • Nút phần tử: <div>, <p>
  • Nút thuộc tính: class="container"
  • Nút văn bản: "Hello World"

Bước 3: Xây dựng cây — thiết lập "quan hệ cha con"

Cuối cùng, trình duyệt dựa vào quan hệ lồng nhau của thẻ, xây dựng cấu trúc cây:

Document (nút gốc tài liệu)
└── html
    └── body
        └── div.class = "container"
            └── p
                └── "Hello World"

3.2 Cây CSSOM: "Sổ Tay Quy Tắc" Của Style

🤔 CSSOM Là Gì?

CSSOM (CSS Object Model), là cấu trúc cây mà trình duyệt chuyển đổi từ quy tắc CSS, dùng để tính toán style cuối cùng của mỗi phần tử.

Bạn có thể tưởng tượng nó như hướng dẫn phối đồ:

  • Quy tắc tầng trên (font của body) sẽ ảnh hưởng đến tầng dưới (tất cả phần tử con)
  • Nếu có xung đột (như cùng một phần tử có nhiều quy tắc chỉ định màu khác nhau), phải quyết định dùng cái nào theo "độ ưu tiên"
  • Cuối cùng tính ra mỗi phần tử nên "mặc" gì

Quá trình xây dựng CSSOM tương tự DOM, nhưng có một khác biệt then chốt: CSS có tính "kế thừa" và "tầng lớp".

Xem quá trình xây dựng CSSOM

CSS gốc:

css
body {
  font-size: 16px;
  color: #333;
}

.container {
  width: 100%;
  color: red;  /* sẽ ghi đè color của body */
}

.container p {
  font-weight: bold;
}

Cây CSSOM sau khi xây dựng:

StyleSheet
├── body
│   ├── font-size: 16px
│   └── color: #333
└── .container
    ├── width: 100%
    ├── color: red  (độ ưu tiên cao hơn, ghi đè color của body)
    └── p
        └── font-weight: bold

3.3 Nhật Ký Vấp Ngã: Tại Sao CSS Của Tôi "Không Có Hiệu Lực"?

Bẫy 1: Xung đột trọng số CSS selector

Xem lỗi phổ biến
css
/* CSS bạn viết */
#header { color: red; }      /* id selector, trọng số 100 */
.title { color: blue; }     /* class selector, trọng số 10 */

/* HTML */
<div id="header" class="title">Đoạn chữ này màu gì?</div>

Bạn nghĩ là màu xanh, kết quả là màu đỏ. Vì trọng số của id selector (100) cao hơn class selector (10).

Bẫy 2: Thẻ HTML không đóng, trình duyệt "tự động sửa"

Xem cách trình duyệt sửa HTML lỗi
html
<!-- HTML bạn viết -->
<div>
  <p>Đây là một đoạn chữ
</div>

<!-- Sau khi trình duyệt sửa -->
<div>
  <p>Đây là một đoạn chữ</p>  <!-- Trình duyệt tự động giúp bạn đóng thẻ -->
</div>

Trình duyệt rất "khoan dung", sẽ tự động sửa lỗi của bạn. Nhưng sự khoan dung này có cái giá — trình duyệt cần tính toán thêm để đoán ý định của bạn, sẽ ảnh hưởng đến hiệu năng.

🌲DOM到渲染树浏览器如何构建渲染树
浏览器需要把 HTML 和 CSS 合并成一棵"渲染树"。想象你在组装家具:图纸是 DOM,说明书是 CSSOM,只有结合两者,才能知道每个零件长什么样、放在哪里。
DOM树
<html>
<head>
<style>
<body>
<div>
<span>
<script>
+
CSSOM树
body
div
color: red
span
display: block
script
display: none
=
渲染树
div
span
可见节点
不可见节点(不包含在渲染树中)
💡核心要点:渲染树只包含可见的节点(display: none 的元素会被忽略)。每个渲染树节点都包含对应的 DOM 节点和计算出的样式信息。渲染树构建完成后,浏览器才能进入布局阶段。

4. Giai Đoạn 2: Xây Dựng Cây Kết Xuất

4.1 Tại Sao Cần "Cây Kết Xuất"?

Bạn có thể hỏi: "Đã có cây DOM và cây CSSOM, tại sao còn phải xây dựng thêm cây kết xuất? Dùng thẳng DOM không được sao?"

Câu trả lời là: Cây DOM chứa quá nhiều thông tin "vô ích".

Ví dụ đoạn HTML sau:

html
<html>
<head>
  <title>Tiêu đề trang</title>
  <style>/* Code CSS */</style>
  <script>/* Code JavaScript */</script>
</head>
<body>
  <div class="container">
    <p>Nội dung hiển thị</p>
  </div>
  <div style="display: none">
    <p>Nội dung ẩn (display:none)</p>
  </div>
</body>
</html>

Cây DOM sẽ chứa tất cả phần tử:

  • <head>, <title>, <style>, <script> (những cái này không hiển thị)
  • div display: none (cũng không hiển thị)

Nhưng cây kết xuất chỉ chứa phần tử "phải vẽ lên màn hình":

  • Bỏ <head> và các phần tử con của nó
  • Bỏ div display: none

4.2 Quy Tắc Xây Dựng Cây Kết Xuất

Trình duyệt khi xây dựng cây kết xuất, sẽ tuân theo một bộ quy tắc:

Tình HuốngCách Xử LýVí DụẢnh Hưởng Hiệu Năng
display: noneLoại trừ hoàn toàn khỏi cây kết xuấtPhần tử và con cháu đều không hiển thị✅ Giảm khối lượng công việc kết xuất
visibility: hiddenVẫn trong cây kết xuất, nhưng không vẽChiếm không gian, nhưng hoàn toàn trong suốt⚠️ Vẫn cần tính toán layout
opacity: 0Vẫn trong cây kết xuất, nhưng trong suốtCó thể tương tác (bấm được), nhưng không thấy⚠️ Vẫn cần tính toán layout
Không trong viewportVẫn trong cây kết xuất, tạm không vẽCuộn đến viewport mới vẽ⚠️ Nhưng vẫn trong cây kết xuất

📊 Bạn Có Thể Thấy Gì Từ Bảng Này?

Phát hiện then chốt: display: none là cách ẩn duy nhất "thực sự tiết kiệm hiệu năng", vì phần tử hoàn toàn không có trong cây kết xuất, trình duyệt sẽ không làm bất kỳ công việc layout và paint nào cho nó.

Còn visibility: hiddenopacity: 0 tuy "không nhìn thấy", nhưng vẫn trong cây kết xuất, trình duyệt vẫn cần tính toán layout của chúng (chiếm không gian). Nếu bạn cần "ẩn nhưng không ảnh hưởng layout" (như làm animation fade in/out), có thể dùng opacity; nếu cần "ẩn hoàn toàn và không chiếm không gian", dùng display: none.

4.3 Nhật Ký Vấp Ngã: Tại Sao Đã Set display:none, Trang Vẫn Giật?

❌ Hiểu Lầm Phổ Biến: Tưởng Phần Tử display:none "Không Tồn Tại"

Nhiều người tưởng set display: none rồi, phần tử "biến mất", thao tác thế nào cũng không ảnh hưởng hiệu năng. Đây là sai lầm!

Mặc dù phần tử display: none không có trong cây kết xuất, nhưng khi bạn dùng JavaScript sửa thuộc tính của nó, trình duyệt vẫn cần:

  1. Tính toán lại style (khớp quy tắc CSS)
  2. Theo dõi thay đổi (chuẩn bị cho hiển thị trong tương lai)

Xem ví dụ "tối ưu" dưới đây:

Xem code "tối ưu vô hiệu"
javascript
// ❌ Bạn nghĩ đây là "tối ưu": ẩn trước, sửa xong hiển thị
const container = document.getElementById('list')
container.style.display = 'none'

// Thao tác DOM điên cuồng
for (let i = 0; i < 1000; i++) {
  const item = document.createElement('div')
  item.style.width = Math.random() * 100 + 'px'  // Thay đổi độ rộng!
  item.textContent = `Item ${i}`
  container.appendChild(item)
}

container.style.display = 'block'

// Vấn đề: mỗi lần sửa style.width, trình duyệt đều phải tính toán lại style,
// ngay cả khi phần tử là display:none!

✅ Tư thế tối ưu đúng:

javascript
// Dùng DocumentFragment thao tác hàng loạt
const container = document.getElementById('list')
const fragment = document.createDocumentFragment()  // Container ảo

// Tất cả thao tác đều trên fragment trong bộ nhớ
for (let i = 0; i < 1000; i++) {
  const item = document.createElement('div')
  item.style.width = Math.random() * 100 + 'px'
  item.textContent = `Item ${i}`
  fragment.appendChild(item)  // Không ảnh hưởng DOM thật
}

// Chèn vào DOM thật một lần, chỉ kích hoạt kết xuất một lần
container.appendChild(fragment)

5. Giai Đoạn 3: Layout và Reflow

5.1 "Layout" Là Gì?

🤔 Layout Là Gì?

Layout, còn gọi là Reflow, là quá trình trình duyệt tính toán "ở vị trí nào, chiếm không gian bao nhiêu" cho mỗi phần tử trong cây kết xuất.

Bạn có thể tưởng tượng nó như nhà thiết kế nội thất đo phòng:

  • Trước tiên đo chiều dài chiều rộng mỗi phòng
  • Quyết định đồ đạc đặt ở đâu
  • Tính ra tọa độ của mỗi món đồ

Tại sao layout "đắt"? Vì sự thay đổi của một phần tử có thể ảnh hưởng đến các phần tử khác. Ví dụ bạn làm một div rộng ra, div bên cạnh nó có thể bị đẩy xuống, dẫn đến toàn bộ trang phải tính toán lại.

5.2 "Bãi Mìn" Kích Hoạt Reflow

Dưới đây là các thao tác phổ biến sẽ kích hoạt reflow, khuyến nghị bookmark và ghi nhớ:

LoạiThuộc Tính/Thao TácẢnh Hưởng Hiệu NăngPhương Án Thay Thế
Kích thướcwidth, height, min/max-width/height💀💀💀Dùng transform: scale() thay thế
Vị trítop, right, bottom, left💀💀💀Dùng transform: translate() thay thế
Lềmargin, padding💀💀Dùng transform hoặc gap thay thế
Viềnborder-width💀💀Tránh sửa đổi thường xuyên
Nội dungThay đổi nội dung văn bản, tải ảnh💀💀Dành sẵn không gian, tránh layout shift
Fontfont-size, line-height💀💀💀Tránh sửa đổi thường xuyên
Hiển thịThay đổi giá trị display💀💀💀Dùng visibility hoặc opacity thay thế (nếu không cần ẩn hoàn toàn)
Truy vấnoffsetWidth, offsetHeight, v.v.💀💀💀💀💀Đọc hàng loạt, tránh layout shift

📊 Bạn Có Thể Thấy Gì Từ Bảng Này?

Phát hiện then chốt:

  1. Thuộc tính hình học (rộng cao vị trí) đắt nhất: Chúng kích hoạt tính toán layout đầy đủ
  2. Thuộc tính truy vấn nguy hiểm hơn sửa đổi: Đọc offsetWidth sẽ ép buộc layout đồng bộ (xem mục 5.4)
  3. transform và opacity có hiệu năng tốt nhất: Chúng không kích hoạt reflow, chỉ kích hoạt composite

5.3 Nhật Ký Vấp Ngã: Tại Sao Animation Của Tôi Giật Như PPT?

Bẫy: Dùng width làm animation

Xem code animation hiệu năng kém
css
/* ❌ Animation xấu: kích hoạt reflow */
.box {
  width: 100px;
  transition: width 0.3s;
}

.box:hover {
  width: 200px;  /* Thay đổi độ rộng kích hoạt reflow! */
}

Mỗi frame animation đều kích hoạt reflow, trình duyệt cần:

  1. Tính toán lại độ rộng
  2. Tính toán lại vị trí (có thể ảnh hưởng phần tử khác)
  3. Vẽ lại

✅ Animation tốt: dùng transform

css
/* ✅ Animation tốt: chỉ kích hoạt composite */
.box {
  width: 100px;
  transform: scaleX(1);
  transition: transform 0.3s;
}

.box:hover {
  transform: scaleX(2);  /* Scale không kích hoạt reflow! */
}

transform được GPU xử lý trực tiếp, không kích hoạt reflow và repaint, animation mượt như lụa.

5.4 Sát Thủ Hiệu Năng: Ép Buộc Layout Đồng Bộ

💀 Vấn Đề Hiệu Năng Nguy Hiểm Nhất: Layout Thrashing

Ép buộc layout đồng bộ (Forced Synchronous Layout), còn gọi là layout thrashing, là vấn đề hiệu năng phổ biến nhất và nghiêm trọng nhất.

Nguyên nhân của nó là: JavaScript khi đọc thuộc tính layout (như offsetWidth), trình duyệt phải ngay lập tức thực thi tính toán layout, mới có thể trả về giá trị chính xác.

Nếu bạn "đọc ghi xen kẽ", sẽ dẫn đến trình duyệt lặp đi lặp lại "layout → đọc → layout → đọc", tạo thành vòng luẩn quẩn.

Xem code layout thrashing
javascript
// ❌ Cực xấu: đọc ghi xen kẽ, dẫn đến layout thrashing
const elements = document.querySelectorAll('.item')

for (let i = 0; i < elements.length; i++) {
  const height = elements[i].offsetHeight  // Đọc → ép buộc layout
  elements[i].style.width = (height * 2) + 'px'  // Ghi → đánh dấu cần reflow
  // Lần đọc của vòng lặp tiếp theo lại ép buộc layout... vòng luẩn quẩn!
}

// Nếu có 100 phần tử, sẽ kích hoạt 100 lần tính toán layout!

✅ Tư thế tối ưu đúng: tách đọc ghi

javascript
const elements = document.querySelectorAll('.item')

// Bước 1: Đọc hàng loạt (đọc hết trước)
const heights = []
for (let i = 0; i < elements.length; i++) {
  heights.push(elements[i].offsetHeight)  // Chỉ kích hoạt layout một lần
}

// Bước 2: Ghi hàng loạt (ghi hết sau)
requestAnimationFrame(() => {
  for (let i = 0; i < elements.length; i++) {
    elements[i].style.width = (heights[i] * 2) + 'px'  // Chỉ kích hoạt reflow một lần
  }
})
📐布局与重排看看布局计算如何影响页面
盒子
邻居元素
触发阶段:
性能影响:-
是否影响其他元素:-
💡核心要点:布局属性(如 width、margin)会触发重排,影响周围元素的位置。而 transform 只触发合成,在 GPU 上处理,不影响其他元素,性能更好。

6. Giai Đoạn 4: Paint và Repaint

6.1 "Paint" Là Gì?

🤔 Paint Là Gì?

Paint, là quá trình trình duyệt thực sự "vẽ" phần tử đã "tính toán layout xong" lên màn hình.

Bạn có thể tưởng tượng nó như sơn phòng:

  • Giai đoạn layout = đo kích thước, kẻ đường
  • Giai đoạn paint = thực sự sơn, dán giấy tường

Paint không đắt như layout, nhưng cũng không rẻ. Paint thường xuyên vẫn ảnh hưởng hiệu năng, đặc biệt là phần tử phức tạp (bóng, gradient, v.v.).

6.2 Tín Hiệu Kích Hoạt Repaint

Khác với reflow, repaint chỉ liên quan đến thay đổi "ngoại quan", không liên quan đến thay đổi "hình học":

LoạiThuộc TínhẢnh Hưởng Hiệu NăngGhi Chú
Màu sắccolor, background-color💀Tác nhân kích hoạt repaint phổ biến nhất
Nềnbackground-image, background-position💀💀Ảnh chậm hơn màu thuần
Viềnborder-color, border-style💀Thay đổi màu/style viền
Chữtext-decoration, text-shadow💀💀Bóng chậm hơn chữ thuần
Box shadowbox-shadow💀💀💀Bóng phức tạp rất chậm
Bo gócborder-radius💀Thay đổi kích thước bo góc
Độ trong suốtopacityĐặc biệt: không kích hoạt repaint, chỉ kích hoạt composite

📊 Bạn Có Thể Thấy Gì Từ Bảng Này?

Phát hiện then chốt: opacity là đặc biệt! Nó giống như transform, không kích hoạt repaint, mà trực tiếp kích hoạt giai đoạn composite. Đây chính là lý do dùng opacity làm animation fade in/out có hiệu năng tốt nhất.

Ngoài ra, bóng và gradient đắt hơn repaint, vì chúng cần tính toán pixel phức tạp. Nếu trang của bạn có nhiều box-shadow, cân nhắc dùng pseudo-element hoặc ảnh thay thế.

6.3 Nhật Ký Vấp Ngã: Tại Sao Hiệu Ứng Hover Của Tôi Giật?

Bẫy: Dùng box-shadow làm animation hover

Xem hiệu ứng hover hiệu năng kém
css
/* ❌ Hiệu ứng hover xấu: animation box-shadow rất chậm */
.card {
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  transition: box-shadow 0.3s;
}

.card:hover {
  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);  /* Bóng rất chậm! */
}

box-shadow cần tính toán từng pixel, animation sẽ giật.

✅ Cách làm tốt: dùng transform hoặc pseudo-element

css
/* ✅ Hiệu ứng hover tốt: dùng transform */
.card {
  transform: translateY(0);
  transition: transform 0.3s, box-shadow 0.3s;
}

.card:hover {
  transform: translateY(-4px);  /* Chỉ đổi bóng khi hover, không làm animation */
  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
}
🎨绘制层优化浏览器如何通过分层提升性能
🖼️背景层
📄内容层
卡片
触发新层的 CSS 属性:
transform: translate3d(0,0,0)任何3D变换都会创建新层
opacity配合transition使用时
position: fixed固定定位元素需要独立层
will-change: transform显式提示浏览器创建层
💡核心要点:浏览器把需要动画的元素提升到独立的 GPU 层,这样动画时只需要调整位置和透明度,不需要重绘。但不要滥用,每个层都会占用 GPU 内存。

7. Giai Đoạn 5: Composite và Tăng Tốc GPU

7.1 "Composite" Là Gì?

🤔 Composite Là Gì?

Composite, là "phép màu" của trình duyệt hiện đại, nó chia các phần khác nhau của trang thành nhiều layer, sau đó dùng GPU (bộ xử lý đồ họa) để song song tổng hợp thành hình ảnh cuối cùng.

Bạn có thể tưởng tượng nó như layer trong Photoshop:

  • Cách truyền thống = tất cả vẽ trên một layer (CPU tuần tự, chậm)
  • Cách composite = vẽ theo layer, cuối cùng hợp nhất (GPU song song, nhanh)

Tại sao composite nhanh? Vì GPU giỏi xử lý các tác vụ song song như "tổng hợp hình ảnh", nhanh hơn CPU hàng chục lần.

7.2 Phần Tử Nào Sẽ Được Nâng Lên "Composite Layer"?

Trình duyệt sẽ tự động nâng một số phần tử lên composite layer độc lập. Dưới đây là các điều kiện kích hoạt phổ biến:

Điều Kiện Kích HoạtThuộc Tính/Giá Trị CSSẢnh Hưởng Hiệu NăngLưu Ý
Biến đổi 3Dtransform: translate3d(), rotate3d()✅✅✅Hiệu năng animation tốt nhất
Hack tăng tốc phần cứngtransform: translateZ(0)✅✅Thường gọi là "ép GPU tăng tốc"
Animation độ trong suốtThay đổi opacity (kèm animation)✅✅✅Không kích hoạt repaint
Fixed positionposition: fixedTránh layout lặp lại khi cuộn
Will-Changewill-change: transform, opacity✅✅Tạo layer trước, chú ý bộ nhớ
Canvas/WebGL<canvas>, nội dung WebGL✅✅Tự nhiên trong layer độc lập
Video<video>✅✅Layer độc lập, tránh ảnh hưởng lẫn nhau

📊 Bạn Có Thể Thấy Gì Từ Bảng Này?

Phát hiện then chốt: transformopacity là thuộc tính animation có hiệu năng tốt nhất, vì chúng không kích hoạt reflow và repaint, trực tiếp kích hoạt composite. Đây chính là lý do hướng dẫn tối ưu hiệu năng luôn nói "dùng transform và opacity làm animation".

Nhưng cần lưu ý: mỗi composite layer đều chiếm bộ nhớ GPU, lạm dụng translateZ(0) sẽ dẫn đến bùng nổ bộ nhớ (xem mục 7.4).

7.3 Nhật Ký Vấp Ngã: Composite Layer Quá Nhiều Lại Giật?

💀 Bẫy Của Tối Ưu Quá Mức

Có người nghe nói "GPU tăng tốc nhanh", liền cho tất cả phần tử thêm transform: translateZ(0), kết quả trang lại càng giật hơn.

Nguyên nhân vấn đề: Mỗi composite layer cần lưu một "texture" (bitmap) trong GPU, chiếm bộ nhớ. Nếu một trang có 100 composite layer, bộ nhớ GPU có thể bị bung, dẫn đến thiết bị cấp thấp crash hoặc giáng cấp xuống CPU rendering.

Xem code "tối ưu quá mức"
css
/* ❌ Cách làm sai: bật GPU tăng tốc cho tất cả phần tử */
.card { transform: translateZ(0); }
.button { transform: translateZ(0); }
.icon { transform: translateZ(0); }
/* ... 100 phần tử đều thêm ... */

/* Kết quả: bộ nhớ GPU bùng nổ, trang đơ */

✅ Cách làm đúng: dùng theo nhu cầu

css
/* Chiến lược 1: chỉ bật cho phần tử thực sự cần animation */
.card {
  transition: transform 0.3s ease;
}

.card:hover {
  transform: translateY(-5px);  /* Tự động tạo composite layer */
}

/* Chiến lược 2: dùng will-change gợi ý trình duyệt */
.card {
  will-change: transform;  /* Tạo layer trước */
}

/* Chiến lược 3: gỡ bỏ sau khi animation kết thúc */
.card:not(:hover) {
  will-change: auto;  /* Giải phóng bộ nhớ GPU */
}
🎬合成层演示浏览器渲染的最后阶段 - 图层合成
合成是浏览器渲染的最后一步。想象你在制作PPT动画:你已经准备好了所有图层,现在只需要调整它们的位置、透明度,然后把它们叠在一起显示出来。这就是合成要做的事情。
🖼️背景层
📄内容层
浮层
合成结果
🖼️
📄
💡核心要点:合成阶段在 GPU 上执行,只调整位置、透明度等,不重新绘制像素。因此 transform 和 opacity 动画性能最好,不会触发重排和重绘。

8. Event Loop: "Thuật Phân Thân" Của JavaScript

🤔 Event Loop Là Gì?

Event Loop, là cơ chế JavaScript thực hiện "bất đồng bộ". Vì JavaScript là đơn luồng (mỗi lần chỉ làm một việc), nhưng nó lại phải xử lý click của người dùng, yêu cầu mạng, timer và nhiều tác vụ khác, nên cần một "hệ thống lập lịch" để quản lý những tác vụ này.

Bạn có thể tưởng tượng nó như trung tâm phân loại chuyển phát nhanh:

  • Call Stack = bưu kiện đang được xử lý
  • Web APIs = kho hợp tác bên ngoài (timer, yêu cầu mạng, v.v.)
  • Callback Queue = kệ bưu kiện chờ xử lý
  • Event Loop = robot phân loại (liên tục kiểm tra "có thể xử lý tác vụ tiếp theo chưa")

8.1 Macro Task và Micro Task

JavaScript thời kỳ đầu chỉ có một hàng đợi tác vụ. Nhưng khi lập trình bất đồng bộ trở nên phức tạp, trình duyệt đã giới thiệu hai loại tác vụ:

LoạiNguồn Phổ BiếnĐộ Ưu TiênThời Điểm Thực Thi
Macro tasksetTimeout/setInterval, thao tác I/O, UI renderingThấpMỗi chu kỳ event loop thực thi một cái
Micro taskPromise.then, MutationObserverCaoSau khi macro task hiện tại kết thúc, lập tức xóa hết tất cả micro task

"Khẩu quyết" thứ tự thực thi:

1. Thực thi macro task hiện tại (ví dụ toàn bộ <script>)
2. Thực thi tất cả micro task sinh ra trong quá trình thực thi (Promise.then, v.v.)
   ↳ Micro task có thể sinh ra micro task mới, xóa hết mới tiếp tục
3. Nếu cần, tiến hành UI rendering (reflow/repaint)
4. Bắt đầu vòng event loop tiếp theo, thực thi macro task tiếp theo

8.2 Nhật Ký Vấp Ngã: Promise Nhanh Hơn setTimeout?

❌ Hiểu Lầm Phổ Biến: setTimeout(fn, 0) Sẽ "Thực Thi Ngay"

Nhiều người tưởng setTimeout(fn, 0) là "0 millisecond sau thực thi ngay", đây là cách hiểu sai lầm.

Thực tế, ý nghĩa của setTimeout(fn, 0) là: "ít nhất đợi 0 millisecond, đưa callback vào hàng đợi macro task". Nhưng nó cần đợi call stack hiện tại trống, hàng đợi micro task trống, UI rendering có thể hoàn thành, mới có thể thực thi.

Xem thứ tự thực thi
javascript
console.log('1. Start')

setTimeout(() => {
  console.log('2. setTimeout callback')
}, 0)

Promise.resolve().then(() => {
  console.log('3. Promise.then')
})

console.log('4. End')

// Thứ tự output bạn nghĩ:
// 1. Start
// 4. End
// 2. setTimeout callback  ← setTimeout(0) không phải là ngay lập tức sao?
// 3. Promise.then

// Thứ tự output thực tế:
// 1. Start
// 4. End
// 3. Promise.then         ← Promise.then thực thi trước setTimeout!
// 2. setTimeout callback

Sơ đồ quy trình thực thi:

Call Stack                     Hàng Đợi Macro Task            Hàng Đợi Micro Task
                              [setTimeout callback]           [Promise.then callback]

1. console.log('1. Start')
   → Output: 1. Start

2. setTimeout(fn, 0)
   → Đưa callback vào hàng đợi macro task  ← [setTimeout callback]

3. Promise.resolve().then()
   → Đưa callback vào hàng đợi micro task                       ← [Promise.then callback]

4. console.log('4. End')
   → Output: 4. End

5. Call stack trống, kiểm tra hàng đợi micro task
   → Phát hiện Promise.then callback
   → Thực thi: console.log('3. Promise.then')
   → Output: 3. Promise.then

6. Hàng đợi micro task trống
   → Có thể cần UI rendering (nếu có thay đổi)

7. Kiểm tra hàng đợi macro task
   → Phát hiện setTimeout callback
   → Thực thi: console.log('2. setTimeout callback')
   → Output: 2. setTimeout callback

💡 Bài Học Cốt Lõi

Micro task "gấp" hơn macro task. Nếu bạn muốn một thao tác nào đó thực thi "sau khi block code hiện tại kết thúc, nhưng trước khi UI cập nhật", dùng Promise.then hoặc queueMicrotask.

setTimeout(0) không đảm bảo thực thi ngay, nó ít nhất sẽ bị trì hoãn đến sau khi call stack hiện tại trống, hàng đợi micro task trống.

Event Loop: How JavaScript Executes Code

Code queue

1
console.log("1")
Executing
2
setTimeout(() => console.log("2"), 0)
3
console.log("3")
4
fetch("/api").then(() => console.log("4"))
5
console.log("5")

Worker (single thread)

👨‍💻
Running
Run console.log("1")

Task queue

No pending tasks

Output log

Waiting for output...

Execution order: not started

Written order: 1, 2, 3, 4, 5

Code is written top to bottom, but it does not always run top to bottom because async work is delayed until the current code finishes.

🔄宏任务与微任务事件循环中的任务优先级
JavaScript 是单线程的,但可以通过任务队列实现异步。就像餐厅只有一个厨师,但他可以同时处理多个订单:先做VIP订单(微任务),再做普通订单(宏任务)。
主线程(执行栈)
同步代码
任务队列
微任务队列(优先级高)
Promise.then()
queueMicrotask()
宏任务队列(优先级低)
setTimeout()
setInterval()
I/O 操作
代码示例
console.log('1')

setTimeout(() => console.log('2'), 0)  // 宏任务

Promise.resolve().then(() => console.log('3'))  // 微任务

console.log('4')

// 输出顺序:1 → 4 → 3 → 2
💡核心要点:每次宏任务执行完后,会清空所有微任务,然后再执行下一个宏任务。这就是为什么 Promise.then() 比 setTimeout() 先执行。

9. Thực Chiến Tối Ưu Hiệu Năng: Làm Cho Trang Web Của Bạn "Bay"

Hiểu quy trình làm việc của pipeline kết xuất rồi, chúng ta cùng xem cách tối ưu. Dưới đây là năm kỹ thuật tối ưu thực dụng nhất.

9.1 Quy Tắc Vàng: Tránh Ép Buộc Layout Đồng Bộ

Vấn đề: Đọc ghi xen kẽ thuộc tính layout, dẫn đến layout thrashing.

Xem so sánh trước và sau tối ưu
javascript
// ❌ Cực xấu: đọc ghi xen kẽ, dẫn đến layout thrashing
for (let i = 0; i < elements.length; i++) {
  const height = elements[i].offsetHeight  // Đọc → ép buộc layout
  elements[i].style.height = (height * 2) + 'px'  // Ghi → đánh dấu cần reflow
  // Lần đọc của vòng lặp tiếp theo lại ép buộc layout... vòng luẩn quẩn!
}

// ✅ Cực tốt: đọc hết trước, ghi hết sau
// Bước 1: Đọc hàng loạt
const heights = []
for (let i = 0; i < elements.length; i++) {
  heights.push(elements[i].offsetHeight)
}

// Bước 2: Ghi hàng loạt
requestAnimationFrame(() => {
  for (let i = 0; i < elements.length; i++) {
    elements[i].style.height = (heights[i] * 2) + 'px'
  }
})

9.2 Dùng transform và opacity Làm Animation

Vấn đề: Dùng width, height, left, top làm animation sẽ kích hoạt reflow.

Xem so sánh trước và sau tối ưu
css
/* ❌ Animation xấu: kích hoạt reflow */
.box {
  transition: width 0.3s, left 0.3s;
}
.box.moving {
  width: 200px;
  left: 100px;
}

/* ✅ Animation tốt: chỉ kích hoạt composite */
.box {
  transition: transform 0.3s;
}
.box.moving {
  transform: translateX(100px) scaleX(2);
}

9.3 Virtual Scrolling: Giải Quyết Danh Sách Dữ Liệu Lớn

Vấn đề: Khi số lượng mục danh sách lên đến hàng nghìn, số nút DOM quá nhiều dẫn đến vấn đề hiệu năng.

Ý tưởng cốt lõi: Chỉ kết xuất các mục danh sách hiển thị trong viewport (cộng thêm một ít buffer), số nút DOM cố định, không liên quan đến tổng lượng dữ liệu.

渲染性能优化让页面丝滑流畅的秘诀
渲染性能优化的目标是每秒60帧(16.67ms/帧)。就像拍电影,每秒帧数越多,画面越流畅。超过这个时间,用户就会感觉卡顿。
❌ 不好的做法
// 触发重排和重绘
function animate() {
element.style.width = '100px'
element.style.height = '100px'
requestAnimationFrame(animate)
}
性能开销
VS
✅ 优化做法
/* 只触发合成 */
function animate() {
element.style.transform = 'translate3d(0,0,0)'
requestAnimationFrame(animate)
}
性能开销
黄金法则:
1️⃣优先使用 transformopacity 做动画
2️⃣避免频繁读取布局属性(如 offsetWidth)
3️⃣使用 will-change 提前告知浏览器
💡核心要点:渲染路径越长,性能越差。最佳路径是:合成(Composite)> 重绘(Paint)> 布局(Layout)> 样式计算(Style)。尽量让动画停留在"合成"阶段,在 GPU 上完成。
Xem triển khai virtual scrolling
vue
<template>
  <div class="virtual-list" @scroll="handleScroll">
    <!-- Phần tử placeholder, chống thanh cuộn -->
    <div class="phantom" :style="{ height: totalHeight + 'px' }"></div>

    <!-- Các mục danh sách thực sự được kết xuất -->
    <div class="content" :style="{ transform: `translateY(${offsetY}px)` }">
      <div
        v-for="item in visibleItems"
        :key="item.id"
        class="item"
        :style="{ height: itemHeight + 'px' }"
      >
        {{ item.name }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const props = defineProps({
  items: Array,
  itemHeight: { type: Number, default: 50 }
})

const scrollTop = ref(0)
const buffer = 5  // Số lượng buffer

// Viewport hiển thị được bao nhiêu mục
const visibleCount = computed(() => 10)

// Chỉ số bắt đầu
const startIndex = computed(() =>
  Math.max(0, Math.floor(scrollTop.value / props.itemHeight) - buffer)
)

// Chỉ số kết thúc
const endIndex = computed(() =>
  Math.min(props.items.length, startIndex.value + visibleCount.value + buffer * 2)
)

// Dữ liệu hiện tại hiển thị
const visibleItems = computed(() =>
  props.items.slice(startIndex.value, endIndex.value)
)

// Tổng chiều cao
const totalHeight = computed(() => props.items.length * props.itemHeight)

// Độ lệch
const offsetY = computed(() => startIndex.value * props.itemHeight)

const handleScroll = (e) => {
  scrollTop.value = e.target.scrollTop
}
</script>

9.4 Debounce và Throttle: Giảm Tần Suất Kích Hoạt Sự Kiện

Vấn đề: Sự kiện kích hoạt thường xuyên (như scroll, resize) dẫn đến vấn đề hiệu năng.

Xem triển khai debounce và throttle
javascript
// Debounce: trì hoãn thực thi, nếu trong thời gian trì hoãn lại kích hoạt, thì đếm lại
function debounce(fn, delay) {
  let timer = null
  return function (...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

// Throttle: thực thi theo khoảng thời gian cố định
function throttle(fn, interval) {
  let lastTime = 0
  return function (...args) {
    const now = Date.now()
    if (now - lastTime >= interval) {
      lastTime = now
      fn.apply(this, args)
    }
  }
}

// Ví dụ sử dụng
window.addEventListener('scroll', debounce(handleScroll, 200))
window.addEventListener('resize', throttle(handleResize, 100))

9.5 Lazy Loading: Trì Hoãn Tải Tài Nguyên Không Quan Trọng

Vấn đề: First screen tải quá nhiều tài nguyên dẫn đến trang mở chậm.

Xem triển khai lazy loading
javascript
// Lazy loading ảnh
const lazyImages = document.querySelectorAll('img[data-src]')

const imageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target
      img.src = img.dataset.src  // Tải ảnh thật
      img.removeAttribute('data-src')
      observer.unobserve(img)  // Dừng quan sát
    }
  })
})

lazyImages.forEach(img => imageObserver.observe(img))

10. Những Vấn Đề Hiệu Năng Bạn Nên Có Thể Nhận Diện

Sau khi hiểu pipeline kết xuất của trình duyệt, bạn nên có thể nhận diện các vấn đề hiệu năng phổ biến sau:

Code Vấn ĐềVấn Đề Nằm Ở ĐâuCách Mô Tả Cho AI
element.style.width = ...Trong vòng lặp thường xuyên sửa độ rộng"Chỗ này sẽ kích hoạt reflow nhiều lần, hãy dùng transform hoặc xử lý hàng loạt"
height = element.offsetHeightĐọc thuộc tính layout ngay sau khi ghi"Đây là ép buộc layout đồng bộ, hãy tách thao tác đọc và ghi"
element.className = ...Thường xuyên sửa class kích hoạt tính toán lại style"Dùng classList.add/remove thay thế, giảm tính toán style"
Animation dùng width/leftKích hoạt reflow và repaint, hiệu năng kém"Hãy dùng transform và opacity làm animation"
Thêm translateZ(0) cho tất cả phần tửLạm dụng GPU tăng tốc dẫn đến bùng nổ bộ nhớ"Chỉ bật GPU tăng tốc cho phần tử cần animation"
Kết xuất toàn bộ 10000 mục danh sáchSố nút DOM quá nhiều dẫn đến giật"Triển khai virtual scrolling, chỉ kết xuất vùng hiển thị"
Thao tác DOM trực tiếp trong sự kiện scrollTần suất kích hoạt quá cao dẫn đến giật"Dùng requestAnimationFrame hoặc throttle để tối ưu"
box-shadow làm animation hoverTính toán bóng phức tạp rất chậm"Dùng transform hoặc pseudo-element, tránh animation bóng"

Nếu bạn đã đọc kỹ "nhật ký vấp ngã" của mỗi chương, bạn còn nắm được những khái niệm cốt lõi sau:

  • Năm giai đoạn pipeline kết xuất: DOM/CSSOM → Cây kết xuất → Layout → Paint → Composite
  • Reflow vs Repaint: Reflow đắt nhất (thay đổi hình học), Repaint đắt thứ hai (thay đổi ngoại quan)
  • Ép buộc layout đồng bộ: Đọc ghi xen kẽ dẫn đến layout thrashing, phải tách ra
  • GPU tăng tốc: transform và opacity được GPU xử lý, hiệu năng tốt nhất
  • Event Loop: JavaScript là đơn luồng, thông qua hàng đợi tác vụ để thực hiện bất đồng bộ

Những khái niệm này sẽ giúp bạn nhanh chóng định vị nút thắt hiệu năng.

💡 Khi Gặp Vấn Đề Hiệu Năng, Hãy Nói Với AI Như Sau

  • "Animation giật, kiểm tra xem có kích hoạt reflow hoặc repaint không"
  • "Hiệu năng cuộn kém, có thể cần throttle hoặc requestAnimationFrame"
  • "Danh sách dữ liệu lớn giật, cần virtual scrolling"
  • "Thường xuyên sửa style dẫn đến vấn đề hiệu năng, hãy dùng transform để tối ưu"

11. Tổng Kết: Bản Chất Của Tối Ưu Pipeline Kết Xuất

Qua bài viết này, chúng ta có thể rút ra những kết luận cốt lõi sau:

Từ góc độ thực tiễn: Không phải tối ưu càng nhiều càng tốt, mà là tối ưu càng "đúng chỗ" càng tốt. Hiểu pipeline kết xuất của trình duyệt, mới biết nên dùng sức ở đâu, nên buông tay ở đâu.

Từ góc độ chi phí:

  • Phần lớn lãng phí hiệu năng đến từ đọc ghi xen kẽ thường xuyên thuộc tính layout, cần thông qua tách đọc ghi, xử lý hàng loạt để giải quyết
  • Hiệu ứng animation phức tạp nếu kích hoạt reflow và repaint, thường bắt nguồn từ việc dùng "thuộc tính sai", cần thông qua transformopacity để giải quyết
  • Đối với kết xuất danh sách dữ liệu lớn, chỉ dựa vào virtual DOM là chưa đủ, phải kết hợp kỹ thuật như virtual scrolling

Mục tiêu là: trong điều kiện trình duyệt và phần cứng cho trước, làm cho mỗi bước kết xuất đều có lợi ích hiệu năng rõ ràng.


12. Bảng Thuật Ngữ

Thuật Ngữ Tiếng AnhDịch Tiếng ViệtGiải Thích
DOMDocument Object ModelCấu trúc cây được hình thành sau khi trình duyệt phân tích tài liệu HTML, JavaScript có thể thao tác phần tử trang qua DOM API
CSSOMCSS Object ModelCấu trúc cây được hình thành sau khi trình duyệt phân tích CSS, kết hợp với DOM để tính toán style cuối cùng
Render TreeCây kết xuấtĐược hợp nhất từ cây DOM và cây CSSOM, chỉ chứa nút hiển thị, dùng cho tính toán layout và vẽ tiếp theo
LayoutLayout/Bố cụcQuá trình tính toán thông tin hình học (vị trí, kích thước) của mỗi nút trong cây kết xuất, còn gọi là Reflow
ReflowReflowKhi thuộc tính hình học của phần tử như kích thước, vị trí thay đổi, trình duyệt cần tính toán lại layout
PaintPaint/VẽQuá trình vẽ style phần tử (màu sắc, nền, viền, v.v.) sau khi tính toán layout lên màn hình
RepaintRepaint/Vẽ lạiKhi thuộc tính ngoại quan của phần tử (như màu sắc, nền) thay đổi nhưng không ảnh hưởng thuộc tính hình học, kích hoạt cập nhật vẽ
CompositeComposite/Tổng hợpQuá trình hợp nhất nhiều layer vẽ thành hình ảnh màn hình cuối cùng, thường thực thi trên GPU
LayerLayer/Lớp tổng hợpBề mặt vẽ độc lập được trình duyệt tạo ra để tối ưu kết xuất, có thể biến đổi và tổng hợp riêng biệt
Event LoopEvent Loop/Vòng lặp sự kiệnCơ chế thực thi bất đồng bộ của JavaScript, chịu trách nhiệm lập lịch thực thi macro task và micro task
Call StackCall Stack/Ngăn xếp gọiCấu trúc dữ liệu ghi lại hàm JavaScript đang được thực thi
Macro TaskMacro Task/Tác vụ macroLoại tác vụ có độ ưu tiên thấp trong event loop, như setTimeout, setInterval, thao tác I/O, v.v.
Micro TaskMicro Task/Tác vụ microLoại tác vụ có độ ưu tiên cao trong event loop, như Promise.then, MutationObserver, v.v.
Forced Synchronous LayoutÉp buộc layout đồng bộTrong JavaScript đọc ghi xen kẽ thuộc tính layout, dẫn đến trình duyệt bị ép phải lập tức thực thi tính toán layout
Layout ThrashingLayout Thrashing/Rung lắc layoutHiện tượng hiệu năng giảm mạnh do ép buộc layout đồng bộ thường xuyên
Virtual ScrollingVirtual Scrolling/Cuộn ảoKỹ thuật chỉ kết xuất các mục danh sách hiển thị trong viewport, dùng để tối ưu hiệu năng danh sách dữ liệu lớn
RAFRequest Animation FrameAPI do trình duyệt cung cấp, dùng để thực thi code JavaScript liên quan đến animation trước lần repaint tiếp theo