Bỏ qua

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!