setTimeout(fn, 0) mà 50ms sau mới chạy? Event loop giải thích
Tóm tắt
JavaScript chỉ có một luồng chính. setTimeout(fn, 0) chỉ đưa callback vào hàng đợi macrotask; nó phải chờ đoạn code hiện tại chạy xong và chờ toàn bộ microtask (Promise.then) được xử lý trước. Vì thế “0ms” trên thực tế có thể là hàng chục mili-giây.
Bạn viết setTimeout(fn, 0) và kỳ vọng hàm chạy “ngay lập tức”. Nhưng đo thực tế thấy nó chạy sau 50ms. Lỗi không nằm ở trình duyệt hay máy tính — mà ở cách event loop của JavaScript hoạt động. JavaScript chỉ có một luồng chính: setTimeout(fn, 0) chỉ đưa callback vào hàng đợi macrotask, nó phải chờ đoạn code hiện tại chạy xong và chờ toàn bộ microtask (Promise.then) được xử lý trước.
Thứ tự thực thi
- Một task đang chiếm call stack 26ms — không ai chen vào được.
- Task đó tạo 1 timer 0ms (vào hàng macrotask) và 3 promise (vào hàng microtask).
- Stack rỗng → event loop xả hết microtask trước, kể cả các
thensinh thêm. - Mới đến lượt macrotask: lúc này timer “0ms” đã chờ 50ms.
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('sync');
// sync → promise → timeout
Mô hình tư duy
| Thành phần | Chứa gì | Khi nào chạy |
|---|---|---|
| Call stack | Code đang chạy | Ngay lập tức, tới khi xong |
| Microtask queue | Promise.then, queueMicrotask | Sau khi stack rỗng, xả hết trước khi sang task khác |
| Macrotask queue | setTimeout, sự kiện UI, I/O | Mỗi vòng event loop lấy một task |
Vì vậy “0ms” nghĩa là “không sớm hơn lúc này” chứ không phải “ngay bây giờ”. Trình duyệt cũng có thể áp thời gian tối thiểu cho timer lồng nhau.
Hệ quả thực tế
- Task dài chặn mọi thứ: giao diện không phản hồi, timer trễ, animation giật.
- Thứ tự không như mong đợi khi trộn
setTimeoutvà Promise. - Microtask vô hạn có thể “bỏ đói” macrotask và làm treo giao diện.
Áp dụng thế nào?
- Đừng dùng
setTimeout(0)để đo thời gian hay giả định thứ tự chính xác — dùng Promise/awaitkhi cần thứ tự rõ ràng. - Chia nhỏ việc nặng: tách thành nhiều phần, dùng
requestIdleCallbackhoặc Web Worker. - Cẩn thận vòng lặp microtask: promise liên tục sinh promise mới.
- Node.js có thêm
process.nextTickvà các phase riêng — nguyên tắc ưu tiên microtask vẫn giữ nguyên.
Mẹo debug
- Dùng tab Performance của DevTools để thấy các task dài (long task).
- Ghi thời gian bằng
performance.now()thay vì giả định. - Khi cần “nhường” cho trình duyệt vẽ lại, cân nhắc
requestAnimationFramehoặc chia task theo từng khung hình.
Nguồn & tham khảo
- The event loop — MDN Web Docs