Microtask và Macrotask: Bí Quyết Làm Chủ Event Loop JavaScript
JavaScript chỉ có một luồng thực thi (single-threaded) — vậy tại sao nó lại xử lý bất đồng bộ tốt đến vậy? Câu trả lời là Event Loop. Nhưng để thực sự hiểu tại sao dòng code này chạy trước dòng kia, bạn cần nắm rõ sự khác biệt giữa Microtask và Macrotask — hai hàng đợi có mức ưu tiên hoàn toàn khác nhau.

Trung Vũ Hoàng
Tác giả
1. Event Loop Hoạt Động Như Thế Nào?
JavaScript thực thi code trong Call Stack. Khi gặp tác vụ bất đồng bộ (setTimeout, fetch...), nó đẩy chúng ra ngoài và tiếp tục chạy code đồng bộ. Khi các tác vụ bất đồng bộ hoàn thành, kết quả không được đẩy thẳng vào Call Stack — mà được xếp vào hàng đợi. Event Loop kiểm tra: nếu Call Stack trống, nó lấy task từ hàng đợi ra thực thi.
Vấn đề là có hai loại hàng đợi với độ ưu tiên hoàn toàn khác nhau: Microtask Queue và Macrotask Queue.
2. Microtask — Ưu Tiên Cao Nhất
Microtask là tác vụ nhỏ, được thực thi ngay lập tức sau khi Call Stack trống — trước khi bất kỳ Macrotask nào được xử lý. Event Loop sẽ quét sạch toàn bộ Microtask Queue trước khi tiếp tục.
Nguồn tạo ra Microtask:
Promise callbacks:
.then(),.catch(),.finally()queueMicrotask()— đẩy trực tiếp vào hàng đợi microMutationObserver— theo dõi thay đổi DOM
Cảnh báo: Nếu một Microtask lại tạo ra Microtask mới, chúng cũng được thực thi ngay. Tạo vòng lặp Microtask vô tận sẽ khiến Event Loop bị kẹt, trình duyệt không thể vẽ UI hay xử lý input từ người dùng.
3. Macrotask — Xếp Hàng Kiên Nhẫn
Macrotask (hay "Task") là các tác vụ hệ thống lớn hơn. Event Loop chỉ lấy một Macrotask mỗi vòng — sau đó phải quét Microtask Queue trước khi lấy Macrotask tiếp theo.
Nguồn tạo ra Macrotask:
setTimeout()/setInterval()setImmediate()(Node.js)Sự kiện I/O: click chuột, gõ phím, nhận dữ liệu mạng
Rendering: trình duyệt vẽ lại giao diện
4. Thứ Tự Thực Thi — Quy Tắc Vàng
Thực thi hết code đồng bộ trong Call Stack.
Quét sạch toàn bộ Microtask Queue.
Lấy một Macrotask thực thi.
Quay lại bước 2 (kiểm tra Microtask mới).
Lặp lại.
5. Ví Dụ "Huyền Thoại" Xuất Hiện Trong Mọi Phỏng Vấn
console.log('1 - Đồng bộ');
setTimeout(() => {
console.log('2 - Macrotask');
}, 0);
Promise.resolve().then(() => {
console.log('3 - Microtask (Promise)');
});
queueMicrotask(() => {
console.log('4 - Microtask (Explicit)');
});
console.log('5 - Đồng bộ cuối');Kết quả in ra:
1 - Đồng bộ
5 - Đồng bộ cuối
3 - Microtask (Promise)
4 - Microtask (Explicit)
2 - MacrotaskNếu bạn hiểu tại sao 5 in trước 3, và 3 in trước 2 — bạn đã nắm 80% cơ chế Event Loop.
6. Ứng Dụng Thực Tế Và Lưu Ý Hiệu Năng
Chia nhỏ tác vụ nặng để UI không bị đơ
Nếu có tính toán chiếm 1 giây, đừng bỏ vào Promise. Dùng setTimeout để chia nhỏ — cho phép trình duyệt chen rendering giữa các task:
function processChunk(data, index) {
if (index >= data.length) return;
// Xử lý một phần
processItem(data[index]);
// Nhường cho trình duyệt render rồi tiếp tục
setTimeout(() => processChunk(data, index + 1), 0);
}Đảm bảo thứ tự sau trạng thái thay đổi
Dùng queueMicrotask() khi cần hành động xảy ra ngay sau trạng thái thay đổi nhưng trước khi bất kỳ Macrotask nào chạy — hữu ích trong các thư viện reactive.
Tránh Microtask Hell
Đệ quy trong Promise sẽ chặn đứng Macrotask Queue mãi mãi — ứng dụng không thể phản hồi click hay scroll từ người dùng nữa.
Tổng Kết
Hiểu Microtask và Macrotask là bước chuyển từ coder sang kỹ sư JavaScript thực thụ:
Microtask → chạy ngay sau mỗi task đồng bộ, ưu tiên tuyệt đối.
Macrotask → xếp hàng chờ, chỉ một task mỗi vòng Event Loop.
Thứ tự: Sync → Microtask → Macrotask → Microtask → Macrotask → ...
Kiến thức này là nền tảng để debug các vấn đề timing trong React, Node.js, và bất kỳ framework JavaScript nào.
Câu hỏi thường gặp
Bài viết liên quan

Zustand Async: 5 Cách Xử Lý Bất Đồng Bộ Hiệu Quả Trong React
Mọi ứng dụng React thực tế đều phải giao tiếp với API bất đồng bộ. Nếu quản lý kém, ứng dụng dễ rơi vào các vấn đề như UI bị đơ, race condition, rò rỉ bộ nhớ, hay hiển thị dữ liệu cũ. Zustand giải quyết bài toán này với một cú pháp cực kỳ đơn giản — định nghĩa async actions trực tiếp trong store, không cần middleware phức tạp như Redux Thunk hay Saga.

Promise Nâng Cao: all, allSettled, race, any — Khi Nào Dùng Cái Nào?
Nếu bạn chỉ dùng async/await tuần tự, ứng dụng của bạn đang bỏ phí tiềm năng hiệu suất — mỗi request phải chờ request trước xong mới bắt đầu. Các phương thức tĩnh của Promise (all, allSettled, race, any) cho phép bạn điều phối nhiều tác vụ bất đồng bộ song song theo chiến lược phù hợp từng bài toán.

Zustand Async: 5 Cách Xử Lý Bất Đồng Bộ Hiệu Quả Trong React
Mọi ứng dụng React thực tế đều phải giao tiếp với API bất đồng bộ. Nếu quản lý kém, ứng dụng dễ rơi vào các vấn đề như UI bị đơ, race condition, rò rỉ bộ nhớ, hay hiển thị dữ liệu cũ. Zustand giải quyết bài toán này với một cú pháp cực kỳ đơn giản — định nghĩa async actions trực tiếp trong store, không cần middleware phức tạp như Redux Thunk hay Saga.