Macrotask vs Microtask trong JavaScript¶
Khái niệm cơ bản¶
Trong JavaScript Event Loop, có hai loại task chính với độ ưu tiên khác nhau:
- Macrotask (Task): Tác vụ "lớn", xử lý từng cái một
- Microtask: Tác vụ "nhỏ", xử lý hàng loạt trong cùng một chu kỳ
Etymology (Nguồn gốc từ ngữ)¶
- Macro = "Lớn" (Tiếng Hy Lạp: μακρός - makrós)
- Micro = "Nhỏ" (Tiếng Hy Lạp: μικρός - mikrós)
Macrotask (Task Queue)¶
Macrotask là các tác vụ có độ ưu tiên thấp hơn, được xử lý từng cái một trong mỗi vòng lặp Event Loop.
Đặc điểm của Macrotask:¶
- Còn gọi là: Task, Callback Task
- Queue: Task Queue (Callback Queue)
- Xử lý: Một task mỗi chu kỳ Event Loop
- Render: Có cơ hội render UI sau mỗi task
- Priority: Thấp hơn Microtask
Các loại Macrotask:¶
// 1. setTimeout / setInterval
setTimeout(() => console.log('setTimeout Macro'), 0);
setInterval(() => console.log('setInterval Macro'), 1000);
// 2. setImmediate (Node.js only)
setImmediate(() => console.log('setImmediate Macro'));
// 3. I/O operations
const fs = require('fs'); // Node.js
fs.readFile('file.txt', (err, data) => {
console.log('File read Macro');
});
// 4. UI Events
document.getElementById('button').addEventListener('click', () => {
console.log('Click event Macro');
});
// 5. MessageChannel / postMessage
const channel = new MessageChannel();
channel.port2.onmessage = () => console.log('MessageChannel Macro');
channel.port1.postMessage('Hello');
// 6. requestAnimationFrame (browser)
requestAnimationFrame(() => console.log('RAF Macro'));
// 7. Web Workers messages
worker.postMessage('data');
worker.onmessage = (e) => console.log('Worker message Macro');
Cách Macrotask được xử lý:¶
console.log('Sync start');
// Tạo 3 macrotasks
setTimeout(() => console.log('Macro 1'), 0);
setTimeout(() => console.log('Macro 2'), 0);
setTimeout(() => console.log('Macro 3'), 0);
console.log('Sync end');
/*
Event Loop execution:
Chu kỳ 1:
- Sync start, Sync end
- Macrotask Queue: [Macro 1, Macro 2, Macro 3]
- Xử lý: Macro 1
- Render có thể xảy ra
Chu kỳ 2:
- Macrotask Queue: [Macro 2, Macro 3]
- Xử lý: Macro 2
- Render có thể xảy ra
Chu kỳ 3:
- Macrotask Queue: [Macro 3]
- Xử lý: Macro 3
- Render có thể xảy ra
Output: Sync start → Sync end → Macro 1 → Macro 2 → Macro 3
*/
Macrotask và UI Rendering:¶
const element = document.getElementById('myElement');
// Mỗi setTimeout tạo ra một macrotask riêng biệt
setTimeout(() => {
element.style.backgroundColor = 'red';
console.log('Changed to red');
// Browser có thể render màu đỏ ở đây
}, 0);
setTimeout(() => {
element.style.backgroundColor = 'blue';
console.log('Changed to blue');
// Browser có thể render màu xanh ở đây
}, 0);
setTimeout(() => {
element.style.backgroundColor = 'green';
console.log('Changed to green');
// Browser có thể render màu xanh lá ở đây
}, 0);
// User có thể thấy element nhấp nháy qua các màu khác nhau
Microtask (Microtask Queue)¶
Microtask là các tác vụ có độ ưu tiên cao, được xử lý hàng loạt trong cùng một chu kỳ Event Loop.
Đặc điểm của Microtask:¶
- Queue: Microtask Queue (Job Queue)
- Xử lý: Tất cả microtasks trong một chu kỳ
- Render: Không có cơ hội render cho đến khi queue rỗng
- Priority: Cao hơn Macrotask
Các loại Microtask:¶
// 1. Promise.then/catch/finally
Promise.resolve().then(() => console.log('Promise.then Micro'));
Promise.reject().catch(() => console.log('Promise.catch Micro'));
// 2. queueMicrotask()
queueMicrotask(() => console.log('queueMicrotask Micro'));
// 3. async/await
async function asyncExample() {
await Promise.resolve();
console.log('async/await Micro');
}
asyncExample();
// 4. MutationObserver (browser)
const observer = new MutationObserver(() => {
console.log('MutationObserver Micro');
});
observer.observe(document.body, { childList: true });
// 5. process.nextTick (Node.js - highest priority)
process.nextTick(() => console.log('nextTick Micro')); // Node.js only
Cách Microtask được xử lý:¶
console.log('Sync start');
// Tạo 3 microtasks
Promise.resolve().then(() => console.log('Micro 1'));
Promise.resolve().then(() => console.log('Micro 2'));
Promise.resolve().then(() => console.log('Micro 3'));
// Tạo 1 macrotask
setTimeout(() => console.log('Macro 1'), 0);
console.log('Sync end');
/*
Event Loop execution:
Chu kỳ 1:
- Sync start, Sync end
- Microtask Queue: [Micro 1, Micro 2, Micro 3]
- Xử lý TẤT CẢ microtasks: Micro 1 → Micro 2 → Micro 3
- Macrotask Queue: [Macro 1]
- Xử lý: Macro 1
- Render có thể xảy ra
Output: Sync start → Sync end → Micro 1 → Micro 2 → Micro 3 → Macro 1
*/
Microtask và UI Rendering:¶
const element = document.getElementById('myElement');
// Tất cả đều là microtasks - xử lý liên tục
Promise.resolve().then(() => {
element.style.backgroundColor = 'red';
console.log('Changed to red');
});
Promise.resolve().then(() => {
element.style.backgroundColor = 'blue';
console.log('Changed to blue');
});
Promise.resolve().then(() => {
element.style.backgroundColor = 'green';
console.log('Changed to green');
});
// Browser chỉ render MỘT LẦN với màu cuối cùng (green)
// User KHÔNG thấy màu đỏ và xanh
Quy trình Event Loop chi tiết¶
Event Loop hoạt động theo thứ tự ưu tiên nghiêm ngặt:
function eventLoopCycle() {
// 1. Thực thi tất cả code đồng bộ (Call Stack)
while (callStack.isNotEmpty()) {
executeNextFunction();
}
// 2. Xử lý TẤT CẢ Microtasks
while (microtaskQueue.isNotEmpty()) {
executeNextMicrotask();
// Nếu microtask tạo ra microtask mới, nó cũng được xử lý ngay
}
// 3. Render UI (nếu cần và nếu là browser)
if (needsRendering()) {
renderUI();
}
// 4. Xử lý MỘT Macrotask (nếu có)
if (macrotaskQueue.isNotEmpty()) {
executeNextMacrotask();
}
// 5. Quay lại bước 2 (kiểm tra microtasks lại)
}
Ví dụ minh họa đầy đủ:¶
console.log('=== START ===');
// Macrotasks
setTimeout(() => console.log('Timeout 1'), 0);
setTimeout(() => console.log('Timeout 2'), 0);
// Microtasks
Promise.resolve().then(() => {
console.log('Promise 1');
// Tạo thêm microtask trong microtask
Promise.resolve().then(() => console.log('Nested Promise 1'));
// Tạo macrotask trong microtask
setTimeout(() => console.log('Timeout 3'), 0);
});
Promise.resolve().then(() => {
console.log('Promise 2');
// Tạo thêm microtask
queueMicrotask(() => console.log('queueMicrotask 1'));
});
queueMicrotask(() => console.log('queueMicrotask 2'));
console.log('=== END ===');
/*
Step-by-step execution:
1. Synchronous code:
=== START ===
=== END ===
2. Process ALL microtasks:
Promise 1
Promise 2
Nested Promise 1 ← Created during microtask execution
queueMicrotask 1 ← Created during microtask execution
queueMicrotask 2
3. Render (nếu cần)
4. Process ONE macrotask:
Timeout 1
5. Process microtasks (none)
6. Render (nếu cần)
7. Process ONE macrotask:
Timeout 2
8. Process microtasks (none)
9. Render (nếu cần)
10. Process ONE macrotask:
Timeout 3 ← Created during step 2
Final Output:
=== START ===
=== END ===
Promise 1
Promise 2
Nested Promise 1
queueMicrotask 1
queueMicrotask 2
Timeout 1
Timeout 2
Timeout 3
*/
Visual Comparison¶
Macrotask Processing (Từng cái một):¶
Event Loop Cycles:
Cycle 1: ┌─────────────┐ ┌─────────────┐
│ Task 1 │ → │ Render │
└─────────────┘ └─────────────┘
Lớn Cơ hội render
Cycle 2: ┌─────────────┐ ┌─────────────┐
│ Task 2 │ → │ Render │
└─────────────┘ └─────────────┘
Lớn Cơ hội render
Cycle 3: ┌─────────────┐ ┌─────────────┐
│ Task 3 │ → │ Render │
└─────────────┘ └─────────────┘
Lớn Cơ hội render
Microtask Processing (Hàng loạt):¶
Event Loop Cycle:
┌───┐┌───┐┌───┐┌───┐┌───┐ ┌─────────────┐
│ M1││ M2││ M3││ M4││ M5│ → (batch) → │ Render │
└───┘└───┘└───┘└───┘└───┘ └─────────────┘
Nhỏ Nhỏ Nhỏ Nhỏ Nhỏ Chỉ render 1 lần
Liên tục, không gián đoạn
Ứng dụng thực tế¶
1. Batching State Updates¶
// React-style state batching simulation
class Component {
constructor() {
this.state = { count: 0 };
this.pendingUpdates = [];
}
setState(update) {
this.pendingUpdates.push(update);
// Schedule microtask to batch updates
queueMicrotask(() => {
if (this.pendingUpdates.length > 0) {
this.flushUpdates();
}
});
}
flushUpdates() {
// Apply all pending updates in one go
this.pendingUpdates.forEach(update => {
this.state = { ...this.state, ...update };
});
this.pendingUpdates = [];
this.render(); // Only render once
}
render() {
console.log('Rendering with state:', this.state);
}
}
const component = new Component();
// Multiple state updates
component.setState({ count: 1 });
component.setState({ count: 2 });
component.setState({ count: 3 });
// Only renders once with final state: { count: 3 }
2. Smooth Animations với Macrotasks¶
function smoothAnimation() {
const element = document.getElementById('animated');
let position = 0;
function animateStep() {
position += 5;
element.style.left = position + 'px';
if (position < 200) {
// Use setTimeout to allow rendering between steps
setTimeout(animateStep, 16); // ~60fps
}
}
animateStep();
}
// vs
function jankyAnimation() {
const element = document.getElementById('animated');
let position = 0;
function animateStep() {
position += 5;
element.style.left = position + 'px';
if (position < 200) {
// Use microtask - blocks rendering
Promise.resolve().then(animateStep);
}
}
animateStep(); // Janky animation, no intermediate renders
}
3. Error Handling Differences¶
// Macrotask errors
setTimeout(() => {
throw new Error('Macrotask error'); // Uncaught error
}, 0);
try {
setTimeout(() => {
throw new Error('Cannot catch this');
}, 0);
} catch (e) {
console.log('This will NOT catch the error');
}
// Microtask errors
Promise.resolve().then(() => {
throw new Error('Microtask error');
}).catch(e => {
console.log('Caught microtask error:', e.message);
});
// Async/await (microtask-based)
async function example() {
try {
await Promise.reject(new Error('Async error'));
} catch (e) {
console.log('Caught async error:', e.message);
}
}
Performance Implications¶
1. Microtask Starvation¶
// DANGER: Infinite microtasks can starve macrotasks
function microtaskStarvation() {
Promise.resolve().then(() => {
console.log('Microtask');
microtaskStarvation(); // Creates infinite microtasks
});
}
// UI will freeze, macrotasks won't execute
microtaskStarvation();
// SOLUTION: Break the chain with macrotask
function fixedMicrotasks() {
Promise.resolve().then(() => {
console.log('Microtask');
setTimeout(fixedMicrotasks, 0); // Use macrotask to break chain
});
}
2. Optimal Task Scheduling¶
// High-priority updates (immediate)
function urgentUpdate() {
queueMicrotask(() => {
updateCriticalUI();
});
}
// Low-priority updates (deferred)
function deferredUpdate() {
setTimeout(() => {
updateNonCriticalUI();
}, 0);
}
// Background processing (even lower priority)
function backgroundTask() {
setTimeout(() => {
processBackgroundData();
}, 100);
}
Browser vs Node.js Differences¶
Browser Priority:¶
// Browser microtask priority:
// 1. queueMicrotask()
// 2. Promise.then/catch/finally
// 3. MutationObserver
queueMicrotask(() => console.log('queueMicrotask'));
Promise.resolve().then(() => console.log('Promise'));
// Output: queueMicrotask → Promise
Node.js Priority:¶
// Node.js microtask priority:
// 1. process.nextTick() - HIGHEST
// 2. Promise.then/catch/finally
// 3. queueMicrotask()
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('Promise'));
queueMicrotask(() => console.log('queueMicrotask'));
// Output: nextTick → Promise → queueMicrotask
Testing và Debugging¶
1. Visualizing Task Execution¶
function logTask(type, name) {
const timestamp = performance.now().toFixed(2);
console.log(`[${timestamp}ms] ${type}: ${name}`);
}
console.log('=== Starting execution ===');
// Macrotasks
setTimeout(() => logTask('MACRO', 'setTimeout 1'), 0);
setTimeout(() => logTask('MACRO', 'setTimeout 2'), 0);
// Microtasks
Promise.resolve().then(() => logTask('MICRO', 'Promise 1'));
queueMicrotask(() => logTask('MICRO', 'queueMicrotask 1'));
console.log('=== Synchronous end ===');
2. Task Queue Monitoring¶
class TaskMonitor {
constructor() {
this.macrotaskCount = 0;
this.microtaskCount = 0;
}
scheduleMacrotask(callback, delay = 0) {
const id = ++this.macrotaskCount;
console.log(`Scheduling macrotask #${id}`);
setTimeout(() => {
console.log(`Executing macrotask #${id}`);
callback();
}, delay);
}
scheduleMicrotask(callback) {
const id = ++this.microtaskCount;
console.log(`Scheduling microtask #${id}`);
queueMicrotask(() => {
console.log(`Executing microtask #${id}`);
callback();
});
}
}
const monitor = new TaskMonitor();
monitor.scheduleMacrotask(() => console.log('Macro work'));
monitor.scheduleMicrotask(() => console.log('Micro work'));
Summary Table¶
| Aspect | Macrotask | Microtask |
|---|---|---|
| Queue | Task Queue | Microtask Queue |
| Priority | Lower | Higher |
| Processing | One per cycle | All per cycle |
| Rendering | After each task | After all tasks |
| Examples | setTimeout, events | Promise.then, queueMicrotask |
| Use case | Heavy work, animations | State updates, cleanup |
| Starvation risk | Low | High (if infinite) |
| Error handling | Separate try/catch needed | Can be caught with Promise.catch |
Kết luận¶
Key Takeaways:¶
- Macrotask: Xử lý "từng khối lớn", có khoảng nghỉ để render
- Microtask: Xử lý "hàng loạt nhỏ", không có khoảng nghỉ
- Priority: Microtask luôn thắng Macrotask
- Rendering: Chỉ xảy ra sau khi Microtask Queue rỗng
Best Practices:¶
- Microtask cho: State updates, cleanup, high-priority work
- Macrotask cho: Heavy computation, animations, UI events
- Avoid infinite microtasks (starvation)
- Monitor task execution để debug performance issues
Nhớ rằng: "Micro thắng Macro, nhưng đừng lạm dụng Micro" - đây là nguyên tắc vàng cho JavaScript task scheduling!