Введение: почему ваш сайт "тормозит", и кто в этом виноват
Представьте себе ресторан, где один официант одновременно принимает заказы, разносит еду, моет посуду и считает выручку. Хаос, правда? Примерно так работает однопоточный JavaScript, если не управлять задачами правильно. Когда мы пишем код, который делает всё сразу — грузит данные, обрабатывает клики, рисует анимацию — браузер замирает, и пользователь видит "задумчивый" интерфейс.
Сегодня мы разберёмся, как очереди задач на JavaScript помогают превратить этот хаос в стройную систему. Вы узнаете, как Event Loop распределяет приоритеты, чем микротаски отличаются от макротасок, и как фоновые задачи JavaScript позволяют не блокировать основной поток. В конце вас ждут практические примеры, которые можно сразу внедрить в свой проект.
Как работает Event Loop: дирижёр оркестра задач
В основе всей асинхронности лежит Event Loop JavaScript. Это механизм, который бесконечно крутится и спрашивает: "Есть что выполнять? А микротаски? А рендеринг?". Если вы когда-нибудь задавались вопросом, почему setTimeout выполняется не сразу, а Promise.then — почти мгновенно — ответ кроется именно здесь.
Стек вызовов (Call Stack)
Это место, куда попадают функции для немедленного выполнения. Пока стек занят, ничего другого не произойдёт. Если ваша функция считает миллион чисел — браузер замрёт, потому что стек не освободится.
Очередь макрозадач (Macrotask Queue)
Сюда попадают setTimeout, setInterval, события DOM (клики, скролл), загрузка ресурсов. Event Loop берёт из этой очереди по одной задаче, выполняет её, потом проверяет микротаски, потом рендерит страницу — и снова берёт следующую макрозадачу.
Очередь микрозадач (Microtask Queue)
А вот это королевство Promise, queueMicrotask, MutationObserver. Микротаски имеют высший приоритет. Как только стек опустеет, Event Loop выполнит ВСЕ микротаски, прежде чем перейти к следующей макрозадаче. Именно поэтому Promise.resolve().then() срабатывает быстрее setTimeout.
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
// Вывод: 1, 4, 3, 2
Этот пример — классика. setTimeout (макротаска) откладывается, даже с нулевой задержкой, а Promise.then (микротаска) выполняется сразу после синхронного кода. Понимание этого порядка — ключ к написанию предсказуемого асинхронного кода.
Фоновые задачи: как не заблокировать интерфейс
А теперь главная проблема: что делать, если нужно выполнить тяжёлую работу (обработка изображений, сложные расчёты, парсинг данных)? Если запустить это в основном потоке — интерфейс встанет колом. Решение — фоновые задачи JavaScript.
Web Workers — настоящая многопоточность
Web Workers JS позволяют запустить код в отдельном потоке. Он не имеет доступа к DOM, но может обмениваться сообщениями с основным потоком. Это идеальный инструмент для обработки очередей JS в фоне.
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ data: [1, 2, 3, 4, 5, 6] });
worker.onmessage = (event) => {
console.log('Результат из воркера:', event.data);
};
// worker.js
self.onmessage = (event) => {
const numbers = event.data.data;
// Тяжелая работа: фильтрация чётных чисел
const result = numbers.filter(n => n % 2 === 0);
self.postMessage(result);
};
Заметьте: основной поток не блокируется. Пользователь может кликать, скроллить, вводить текст — всё будет работать плавно, пока воркер считает.
setTimeout и setInterval: простая очередь с задержкой
Иногда не нужна полноценная многопоточность, а просто нужно отложить выполнение. Отложенные задачи JavaScript с помощью setTimeout — самый простой способ создать очередь. Но помните: точность не гарантируется, особенно если стек занят.
function createTaskQueue() {
const queue = [];
function processNext() {
if (queue.length === 0) return;
const task = queue.shift();
task();
// Планируем следующую задачу, давая браузеру вздохнуть
setTimeout(processNext, 0);
}
return {
add(task) {
queue.push(task);
if (queue.length === 1) {
setTimeout(processNext, 0);
}
}
};
}
const queue = createTaskQueue();
queue.add(() => console.log('Задача 1'));
queue.add(() => console.log('Задача 2'));
Этот паттерн не блокирует интерфейс, потому что между задачами Event Loop успевает обработать рендеринг и пользовательские события.
Практические сценарии: где это применять
1. Загрузка изображений с прогрессом
Представьте, что пользователь загружает 50 фотографий. Если обрабатывать их все сразу — браузер зависнет. Используем очередь микрозадач JavaScript для пошаговой обработки.
function processImagesInQueue(images) {
const results = [];
let index = 0;
function processNext() {
if (index >= images.length) {
console.log('Все изображения обработаны:', results);
return;
}
// Имитация обработки
const img = images[index];
results.push(`Обработано: ${img}`);
index++;
// Планируем следующую итерацию как микротаску
queueMicrotask(processNext);
}
processNext();
}
processImagesInQueue(['img1.jpg', 'img2.jpg', 'img3.jpg']);
Используя queueMicrotask, мы даём приоритет обработке, но не блокируем основной поток надолго.
2. Ленивая загрузка компонентов
В больших SPA-приложениях часто используют планировщик задач JavaScript для отложенной загрузки некритичных компонентов. Например, виджет чата или аналитику можно загрузить через setTimeout после того, как отрисовался основной контент.
// Загружаем аналитику через 2 секунды после загрузки страницы
window.addEventListener('load', () => {
setTimeout(() => {
import('./analytics.js').then(module => {
module.init();
});
}, 2000);
});
3. Throttling и Debouncing
Это классические примеры обработки очередей JS для оптимизации событий. Throttling гарантирует, что функция выполняется не чаще одного раза в N миллисекунд. Debouncing — что функция выполняется только после паузы в событиях.
Блоги
Рекомендаций по книгам
Читайте также