Понимание того, как JavaScript выполняет код, является фундаментальным для написания производительных приложений. Многие разработчики сталкиваются с ситуацией, когда код выполняется не в том порядке, в котором они ожидали, особенно при работе с асинхронными операциями. В этой статье мы разберем механизм Event Loop, разницу между микрозадачами и макрозадачами, а также то, как это влияет на рендеринг страницы.
Однопоточность и асинхронность
JavaScript является однопоточным языком. Это означает, что он может выполнять только одну операцию за раз. Если бы не было механизма асинхронности, любой долгий запрос к серверу или тяжелое вычисление замораживали бы весь интерфейс. Для решения этой проблемы используется Event Loop (цикл событий), который координирует выполнение кода, колбэков и событий.
В основе лежит Call Stack (стек вызовов). Когда функция вызывается, она помещается в стек. Когда функция завершается, она убирается из стека. Но что происходит с setTimeout или fetch? Они не выполняются сразу. Они передаются в Web API (в браузере) или C++ API (в Node.js), а их колбэки попадают в очереди задач.
Микрозадачи против Макрозадач
Самый важный нюанс для разработчика — различие между очередью микрозадач (Microtasks) и очередью макрозадач (Macrotasks).
- Макрозадачи:
setTimeout,setInterval,setImmediate(Node.js), I/O операции, UI рендеринг. - Микрозадачи:
Promise.then/catch/finally,queueMicrotask,MutationObserver.
Правило Event Loop гласит: после выполнения всего синхронного кода из стека, сначала выполняется вся очередь микрозадач, и только потом — одна макрозадача. После одной макрозадачи цикл повторяется.
console.log('1. Старт');
setTimeout(() => {
console.log('2. Timeout (Macrotask)');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise (Microtask)');
});
console.log('4. Конец');
// Вывод:
// 1. Старт
// 4. Конец
// 3. Promise (Microtask)
// 2. Timeout (Macrotask)
Влияние на рендеринг и производительность
Понимание очередей критично для оптимизации UI. Браузер пытается обновлять экран (paint) после каждой макрозадачи. Если вы заполните очередь микрозадач бесконечным циклом промисов, рендеринг никогда не произойдет, и браузер «зависнет».
// Плохой пример: блокировка рендеринга
function blockRender() {
Promise.resolve().then(blockRender);
}
blockRender(); // Браузер не сможет отрисовать кадр
// Хороший пример: использование setTimeout для разгрузки
function yieldToBrowser() {
// Даем браузеру отрисовать кадр
setTimeout(() => {
// Продолжаем работу
}, 0);
}
Используйте микрозадачи для атомарных операций, которые должны выполниться до перерисовки (например, обновление состояния перед рендером React). Используйте макрозадачи для отложенных вычислений, чтобы не блокировать основной поток.