Mới54 bài học mới được bổ sung từ 10/09!
Xem nhật ký cập nhật →
Software Development Atlas
Lập trìnhLập trình bất đồng bộ

Cơ chế hoạt động của Event Loop trong Trình duyệt

Hiểu sâu sắc về task queue, microtask, checkpoint, rendering pipeline, đói tài nguyên và sự khác biệt căn bản so với Node.js.

Phát triểnĐã xác minh: 5 thg 11, 2026Đánh giá lại: 180 ngày

Bản đồ học tập phát triển phần mềm bởi Tran Trong Thuc · Về dự án Atlas · Cập nhật lần cuối: 5 thg 11, 2026

Tóm tắt nhanh (TL;DR)

Event loop điều phối các task, microtask và cơ hội cập nhật giao diện trong trình duyệt. Nếu một microtask liên tục tạo thêm microtask mới, trình duyệt có thể không có cơ hội xử lý tương tác hoặc vẽ khung hình tiếp theo. Để hiểu nguyên nhân, hãy bắt đầu bằng thứ tự thực thi quan sát được, rồi phân biệt những điều được đặc tả với lựa chọn của bộ lập lịch.

💡 Quy tắc bỏ túi: Mã JavaScript đồng bộ trong một task chạy đến khi kết thúc. Khi đến microtask checkpoint, trình duyệt xử lý các microtask đã xếp hàng, kể cả microtask phát sinh trong lúc xử lý. Sau đó event loop có thể chọn task tiếp theo hoặc thực hiện bước cập nhật kết xuất nếu thích hợp. Đừng coi mọi nguồn task là một FIFO toàn cục.

  • 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ẻ.
  • Lỗi dễ gặp (microtask starvation): Một chuỗi microtask liên tục tự thêm việc mới có thể ngăn trình duyệt nhận input và cập nhật giao diện. Muốn chia nhỏ công việc nặng, hãy nhường quyền qua cơ chế scheduling thích hợp thay vì tạo thêm microtask.

Dự đoán thứ tự thực thi từ một ví dụ nhỏ

Trong trang web, đoạn mã sau in gì trước?

console.log("A");
setTimeout(() => console.log("timer"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("B");
A
B
promise
timer

Hai câu lệnh đồng bộ chạy trước. Handler của Promise chạy trong microtask sau phần mã đồng bộ; callback của timer là một task chạy sau đó. setTimeout(..., 0) không có nghĩa callback được chạy ngay lập tức.

Ba vùng làm việc: Call Stack, Task Queues và Microtask Queue

Các vùng công việc của Browser Event Loop

Task hiện tại

Call stack
JavaScript chạy đến hết

Microtask queue

Promise reaction
queueMicrotask()

Công việc browser sau đó

Task queue
Timer, input, network
Rendering opportunity
Có thể cập nhật frame
JavaScript hiện tại chạy đến hết; microtask được xả tại checkpoint; task sau và rendering chờ đến lượt.

Chu trình làm việc của Microtask Checkpoint

Microtask checkpoint xả cho đến khi rỗng
  1. Task hiện tại kết thúc
  2. Chạy microtask tiếp theo
  3. Có microtask mới?
    Nếu có, tiếp tục xả
  4. Queue rỗng
    Browser có thể tiếp tục scheduling / rendering
Một microtask có thể enqueue microtask khác và công việc mới có thể chạy trong cùng checkpoint trước task sau.
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:

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

Timer chỉ hẹn thời điểm sớm nhất

setTimeout(fn, 0) không cam kết callback chạy đúng sau 0 mili-giây: nó vẫn đợi công việc hiện tại hoàn tất, các microtask liên quan và lịch xử lý task của trình duyệt. Trình duyệt còn có chính sách riêng cho timer lồng nhau và tab chạy nền.

Nhiều task source không đồng nghĩa một hàng FIFO

Các task do timer, tương tác người dùng và networking tạo ra có thể thuộc nguồn khác nhau. Thứ tự giữa các nguồn này phụ thuộc vào quy tắc của HTML và lựa chọn của trình duyệt. Vì vậy, không nên khẳng định mọi timer luôn chạy trước hoặc sau mọi sự kiện input. Hãy thể hiện thứ tự bắt buộc bằng quan hệ phụ thuộc rõ ràng.

Promise job trở thành microtask thế nào?

ECMAScript định nghĩa Promise reaction job; môi trường trình duyệt tích hợp việc lên lịch các job đó với microtask checkpoint của HTML. Handler .then() vì thế không chạy đồng bộ khi vừa đăng ký. Đây là mô hình hữu ích để dự đoán trace, nhưng không nên áp dụng nguyên xi sang Node.js vì runtime đó có thêm cơ chế scheduling riêng.

Đườ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).

Vòng đời một frame của trình duyệt
  1. Task + microtask checkpoint
  2. Rendering opportunity
    Browser có thể bỏ qua
  3. requestAnimationFrame
    Trước style/layout của frame
  4. Style → layout → paint
  5. Composite / present
Rendering là cơ hội do browser lập lịch; không được đảm bảo xảy ra sau mọi task hay microtask.

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:

Phòng thực hành Event Loop

Từng bước khám phá các kịch bản lập lịch tác vụ của trình duyệt. Trình mô phỏng mô hình hóa chuyển đổi trạng thái phục vụ học tập, không chạy mã JavaScript tùy ý.
Run-to-completion first, then a microtask checkpoint before the later timer task.
console.log('A');
setTimeout(() => console.log('timer'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('B');
Bước 0
Trạng thái: Đang thực thi tác vụ

Tác vụ đang thực thi

initial script

Hàng đợi Microtasks

Trống

Công việc liên quan đến dựng hình

Trạng thái dựng hình: Đang thực thi tác vụ

Các hàm gọi lại requestAnimationFrame

Trống

Tác vụ sẵn sàng theo nguồn

Mã kịch bản (Script)

Trống

Bộ đếm giờ (Timer)

Trống

Tương tác người dùng

Trống

Mạng (Networking)

Trống

Dựng hình (Rendering)

Trống

Các luồng này gom nhóm công việc theo nguồn tác vụ phục vụ giải thích. Điều này không có nghĩa là mỗi nguồn tác vụ tương ứng 1-1 với một hàng đợi tác vụ của trình duyệt.

Nhật ký kết quả

Chưa có kết quả

Giải thích bước này

The initial script is the currently selected work.


Hiện tượng đói tài nguyên (Starvation) và đóng băng Main Thread

Hai cách làm nghẽn main thread

Synchronous task dài

CPU work · 400 ms
Input chờ trong task queue
Không frame nào được present

Chuỗi microtask vô hạn

Mỗi microtask rất nhỏ
Mỗi cái lại queue cái khác
Checkpoint không bao giờ rỗng
Một synchronous task dài chặn tiến trình trực tiếp; chuỗi microtask vô hạn khiến checkpoint không thể kết thúc.

Tình huống minh họa (giả định): giao diện không phản hồi 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:

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:
    // 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

Đặc tínhTrình duyệt (Browser)Node.js
Đặc tả quy chuẩnWHATWG HTML StandardNode.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ốngscheduler.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

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

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');
Xem giải thích chi tiết

Thứ tự in ra:

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.

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

Mục lục bài học