Tips

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.

Thứ tự thực thi

  1. Một task đang chiếm call stack 26ms — không ai chen vào được.
  2. Task đó tạo 1 timer 0ms (vào hàng macrotask) và 3 promise (vào hàng microtask).
  3. Stack rỗng → event loop xả hết microtask trước, kể cả các then sinh thêm.
  4. 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

Á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/await khi cần thứ tự rõ ràng.
  • Chia nhỏ việc nặng: task dài chặn UI và làm trễ mọi timer; tách thành nhiều phần, dùng requestIdleCallback hoặc Web Worker.
  • Cẩn thận microtask vô hạn: promise liên tục sinh promise mới có thể “bỏ đói” macrotask và làm treo giao diện.
  • Node.js có thêm process.nextTick và các phase riêng — nguyên tắc ưu tiên microtask vẫn giữ nguyên.