# Cơ chế hoạt động của Event Loop trong Trình duyệt (/vi/docs/programming/async/how-the-browser-event-loop-works)



## Tóm tắt nhanh (TL;DR) [#tóm-tắt-nhanh-tldr]

Một vòng lặp microtask đệ quy vô tận bằng `Promise.resolve().then(...)` hay `queueMicrotask()` có thể lập tức làm tê liệt toàn bộ tab trình duyệt hoặc đóng băng tiến trình Node.js mà không cần đến bất kỳ vòng lặp `while(true)` đồng bộ nào. Trình duyệt tuyệt đối không nhường quyền cho các tác vụ người dùng, bộ hẹn giờ Macrotask hay chu trình vẽ lại màn hình (Render Queue) chừng nào hàng đợi Microtask chưa được vét sạch 100%. Hiểu rõ cách Call Stack, Microtasks, Tasks/Macrotasks và Rendering Pipeline đan kết vào nhau là ranh giới giữa một ứng dụng mượt mà 60fps và một giao diện "chết đứng".

> 💡 &#x2A;*Quy tắc bỏ túi:** Mã đồng bộ trên Call Stack chạy đến khi cạn kiệt trước tiên, ngay sau đó trình duyệt vét sạch toàn bộ Microtask Queue về con số 0. Chỉ khi hàng đợi Microtask hoàn toàn trống rỗng, trình duyệt mới cân nhắc Cơ hội Kết xuất (Rendering Opportunity) hoặc chọn một Macrotask tiếp theo từ Task Source.

* **Call Stack tuân thủ nguyên tắc Run-to-Completion:** Đoạn mã JavaScript đồng bộ đang chạy trên ngăn xếp sẽ luôn thực thi trọn vẹn đến cùng mà không bị ngắt quãng bởi bất kỳ callback sự kiện nào khác.
* **Microtask Checkpoint luôn vét cạn 100%:** Các phản hồi Promise (`.then`, `.catch`) và `queueMicrotask()` được xử lý ngay khi Call Stack trống rỗng. Mọi microtask mới phát sinh trong quá trình này cũng sẽ bị hút vào và xử lý dứt điểm ngay trong cùng một checkpoint.
* **Rendering hoàn toàn tách biệt khỏi chu kỳ Task:** Trình duyệt không vẽ lại sau mỗi callback. Quá trình cập nhật giao diện diễn ra tại các **Rendering Opportunity** đồng bộ theo tần số quét màn hình, và `requestAnimationFrame()` được kích hoạt ngay trước các bước tính toán Style và Layout.
* **Macrotasks đến từ nhiều nguồn tác vụ (Task Sources) độc lập:** Bộ hẹn giờ (`setTimeout`), tương tác người dùng và sự kiện mạng nằm trong các hàng đợi riêng biệt do bộ điều phối trình duyệt quản lý, không phải một hàng đợi FIFO đơn lẻ.
* **Cạm bẫy chết người (Đói tài nguyên do Microtask - Microtask Starvation):** Việc gọi đệ quy microtask liên tục sẽ giam cầm luồng chính trong một checkpoint vô tận, bỏ đói hoàn toàn Macrotask Queue và Render Queue. Giao diện người dùng sẽ bị đóng băng cứng ngắc và tê liệt mọi tương tác click chuột.

***

## Ba vùng làm việc: Call Stack, Task Queues và Microtask Queue [#ba-vùng-làm-việc-call-stack-task-queues-và-microtask-queue]

<AtlasIllustration id="event-loop-architecture" />

<TermBox term="Task source">
  Một &#x2A;*Task source (nguồn tác vụ)** theo đặc tả HTML là một nguồn phát sinh các tác vụ cụ thể, ví dụ như thao tác DOM, tương tác người dùng, sự kiện mạng hoặc bộ hẹn giờ. Trình duyệt có quyền lựa chọn hàng đợi tác vụ nào để xử lý tiếp theo nhằm tối ưu hóa độ phản hồi của giao diện.
</TermBox>

***

## Chu trình làm việc của Microtask Checkpoint [#chu-trình-làm-việc-của-microtask-checkpoint]

<TermBox term="Microtask checkpoint">
  Một **Microtask checkpoint** là thời điểm trình duyệt tạm dừng mọi công việc khác để xử lý dứt điểm toàn bộ microtask đang chờ. Nếu một microtask lại tạo ra microtask mới, microtask mới đó cũng sẽ được xử lý ngay trong cùng checkpoint này.
</TermBox>

<AtlasIllustration id="microtask-checkpoint-drain" />

```js
console.log('Script bắt đầu');

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

Promise.resolve().then(() => {
  console.log('Promise microtask 1');
}).then(() => {
  console.log('Promise microtask 2 (lồng nhau)');
});

console.log('Script kết thúc');
```

**Thứ tự in ra màn hình:**

```text
Script bắt đầu
Script kết thúc
Promise microtask 1
Promise microtask 2 (lồng nhau)
Timer callback
```

***

## Đường ống kết xuất giao diện (Rendering Pipeline) [#đường-ống-kết-xuất-giao-diện-rendering-pipeline]

Một sai lầm rất phổ biến là nghĩ rằng trình duyệt sẽ vẽ lại màn hình ngay sau mỗi tác vụ hay microtask. Thực tế, việc vẽ lại hoàn toàn độc lập và được điều phối bởi chu kỳ quét của màn hình (Refresh Rate).

<AtlasIllustration id="browser-rendering-pipeline" />

<TermBox term="Rendering opportunity">
  **Rendering opportunity (Cơ hội kết xuất)** là thời điểm trình duyệt quyết định có cần gửi một frame mới đến màn hình hay không. Nếu không có thay đổi hình ảnh hoặc tab đang chạy nền, trình duyệt sẽ bỏ qua bước này để tiết kiệm năng lượng.
</TermBox>

***

## Phòng thí nghiệm Event Loop (Interactive Lab) [#phòng-thí-nghiệm-event-loop-interactive-lab]

Trải nghiệm trực quan cách Event Loop điều phối giữa Call Stack, Microtasks, Timers và Rendering Pipeline:

<EventLoopLab />

***

## Hiện tượng đói tài nguyên (Starvation) và đóng băng Main Thread [#hiện-tượng-đói-tài-nguyên-starvation-và-đóng-băng-main-thread]

<AtlasIllustration id="main-thread-starvation" />

***

## Sự cố thực tế: Đóng băng giao diện "vô hình" do Microtask [#sự-cố-thực-tế-đóng-băng-giao-diện-vô-hình-do-microtask]

Một ứng dụng web chat realtime xử lý hàng loạt tin nhắn nhận về qua WebSocket. Khi một lô tin nhắn 500 items ùa về, lập trình viên sử dụng một hàm đệ quy để xử lý từng tin nhắn:

```ts
function processMessageBatch(messages: Message[]) {
  if (messages.length === 0) return;
  const msg = messages.shift()!;
  renderMessage(msg);
  // Cố gắng "chia nhỏ" bằng queueMicrotask thay vì xử lý loop đồng bộ:
  queueMicrotask(() => processMessageBatch(messages));
}
```

* **Hậu quả:** Dù không có một vòng lặp `for` dài gây blocking Call Stack, UI vẫn bị "đóng băng" suốt 1.2 giây! Người dùng nhấp chuột vào nút "Gửi" hay thanh nhập liệu đều hoàn toàn bất động. Chỉ số INP (Interaction to Next Paint) báo đỏ nghiêm trọng (>1000ms).
* **Nguyên nhân cốt lõi:** Lập trình viên nhầm lẫn giữa `queueMicrotask()` và việc nhường luồng (yielding) cho trình duyệt. `queueMicrotask()` **không bao giờ** nhường quyền điều khiển cho Task Queue hay Rendering Pipeline; nó tiếp tục giữ chặt checkpoint cho đến khi queue rỗng 100%.
* **Cách khắc phục chuẩn:** Nếu muốn nhường luồng cho trình duyệt vẽ lại UI và nhận input click, phải đẩy công việc ra Macrotask hoặc sử dụng scheduler chuẩn:
  ```ts
  // Giải pháp hiện đại: scheduler.yield() nếu hỗ trợ, dự phòng sang setTimeout 0
  async function processMessageBatch(messages: Message[]) {
    for (const msg of messages) {
      renderMessage(msg);
      if ('scheduler' in window && 'yield' in (window as any).scheduler) {
        await (window as any).scheduler.yield();
      } else {
        await new Promise((resolve) => setTimeout(resolve, 0));
      }
    }
  }
  ```

***

## So sánh Event Loop giữa Trình duyệt và Node.js [#so-sánh-event-loop-giữa-trình-duyệt-và-nodejs]

| Đặc tính                         | Trình duyệt (Browser)                              | Node.js                                       |
| :------------------------------- | :------------------------------------------------- | :-------------------------------------------- |
| **Đặc tả quy chuẩn**             | WHATWG HTML Standard                               | Node.js Runtime Architecture (dựa trên libuv) |
| **Giao diện & Hiển thị**         | Có Rendering Pipeline và `requestAnimationFrame()` | Không có rendering pipeline                   |
| **API hoãn tác vụ cấp hệ thống** | `scheduler.yield()`, `requestIdleCallback()`       | `setImmediate()`, `process.nextTick()`        |
| **Quy tắc ưu tiên**              | Ưu tiên tính phản hồi của người dùng và khung hình | Ưu tiên thông lượng I/O và tính toán server   |

***

## Bài tập kiểm tra tư duy [#bài-tập-kiểm-tra-tư-duy]

Hãy dự đoán thứ tự in ra console của đoạn mã sau:

```ts
console.log('A');

setTimeout(() => {
  console.log('timer');
}, 0);

Promise.resolve().then(() => {
  console.log('promise');

  queueMicrotask(() => {
    console.log('nested microtask');
  });
});

queueMicrotask(() => {
  console.log('queued microtask');
});

console.log('B');
```

<details>
  <summary>
    Xem giải thích chi tiết
  </summary>

  **Thứ tự in ra:**

  ```text
  A
  B
  promise
  queued microtask
  nested microtask
  timer
  ```

  **Giải thích:**

  1. Đồng bộ chạy trước: `A`, rồi `B`.
  2. Microtask Checkpoint bắt đầu:
     * Microtask đầu tiên: in `promise`, đồng thời đẩy `nested microtask` vào cuối hàng đợi microtask.
     * Microtask tiếp theo đã xếp hàng sẵn từ trước: in `queued microtask`.
     * Microtask mới sinh: in `nested microtask`.
  3. Chỉ sau khi Microtask Queue cạn kiệt hoàn toàn, Event Loop mới chuyển sang nhặt task từ Timer Queue: in `timer`.
</details>

***

## Checklist rà soát mã nguồn cho Kỹ sư [#checklist-rà-soát-mã-nguồn-cho-kỹ-sư]

* [ ] **Thời lượng tác vụ (Task Duration):** Mọi tác vụ trên Main Thread có hoàn thành dưới ngưỡng 50ms (ngưỡng Long Task trong DevTools) không?
* [ ] **Giới hạn Microtask:** Các lời gọi `queueMicrotask()` hoặc chuỗi Promise đệ quy có điểm dừng rõ ràng để thoát khỏi checkpoint không?
* [ ] **Không phỏng đoán thứ tự Task Source:** Tránh dựa vào giả định rằng timer sẽ luôn chạy trước hay sau một sự kiện người dùng không liên quan.
* [ ] **Lên lịch hoạt ảnh chuẩn:** Các thao tác cập nhật DOM liên tục hoặc animation đã được đưa vào `requestAnimationFrame()` chưa?
* [ ] **Phân tách ranh giới môi trường:** Đảm bảo không mang các API đặc thù của Node (`process.nextTick`, `setImmediate`) sang code chạy trên trình duyệt.

***

## Tài liệu quy chuẩn [#tài-liệu-quy-chuẩn]

* [WHATWG HTML Standard — Event Loops](https://html.spec.whatwg.org/multipage/webappapis.html#event-loops) — Đặc tả quy chuẩn thế giới về Event Loop, hàng đợi tác vụ và microtask checkpoint trên trình duyệt.
* [W3C Prioritized Task Scheduling API](https://wicg.github.io/scheduling-apis/) — Đặc tả chuẩn về `scheduler.yield()` và `scheduler.postTask()`.
