Способи поділу тривалих завдань у JavaScript
JavaScript — це однопотокова мова програмування, що працює на основі event loop (циклу подій). Це означає, що виконання складних або тривалих обчислень може заблокувати головний потік, зробивши додаток непрацездатним (UI перестане відповідати, а користувачі побачать “зависання”). Тому вкрай важливо правильно ділити тривалі завдання, щоб не порушувати плавність роботи застосунку.
У цій статті ми розглянемо основні способи розбиття довготривалих завдань у JavaScript, їхні переваги та недоліки.
Проблема блокування головного потоку
Веб-додатки працюють у середовищі, де кожна задача виконується в основному потоці. Якщо якийсь код займає надто багато часу (наприклад, складні обчислення або обробка великої кількості даних), це блокує подальше виконання подій, включаючи анімації, введення користувача та відповіді на HTTP-запити.
Приклад проблеми:
// Симуляція довготривалої обчислювальної задачі
function longTask() {
let sum = 0;
for (let i = 0; i < 1e9; i++) { // Величезний цикл
sum += i;
}
console.log("Task completed", sum);
}
// Виклик блокує головний потік
longTask();
console.log("Цей код виконається лише після завершення довгої задачі.");
Результат:
- Браузер “зависає” на кілька секунд.
- Жодна інша подія не може бути виконана.
Як цього уникнути? Ось кілька основних способів.
1. Використання setTimeout
або setInterval
Одним із простих способів розділити виконання тривалих завдань є розбиття задачі на менші частини та виконання їх у наступних ітераціях циклу подій.
Приклад:
function longTaskChunked() {
let sum = 0;
let i = 0;
function processChunk() {
let start = Date.now();
while (i < 1e9) {
sum += i;
i++;
// Виходимо з циклу, якщо витратили більше 50 мс
if (Date.now() - start > 50) {
console.log("Chunk processed");
setTimeout(processChunk, 0); // Відкладаємо наступну ітерацію
return;
}
}
console.log("Task completed", sum);
}
processChunk();
}
longTaskChunked();
Переваги:
- Не блокує основний потік.
- Користувач може продовжувати взаємодіяти з UI.
Недоліки:
- Не дає реального багатопотокового виконання.
- Витрати на перемикання контексту можуть уповільнювати виконання.
2. Використання requestIdleCallback
Функція requestIdleCallback
дозволяє виконувати задачі, коли браузер має вільний час, тобто між основними подіями.
Приклад:
function longTaskIdle(deadline) {
let sum = 0;
let i = 0;
function processChunk() {
while (i < 1e9 && deadline.timeRemaining() > 0) {
sum += i;
i++;
}
if (i < 1e9) {
requestIdleCallback(processChunk);
} else {
console.log("Task completed", sum);
}
}
requestIdleCallback(processChunk);
}
longTaskIdle();
Переваги:
- Виконується тільки тоді, коли браузер не зайнятий.
- Не впливає на продуктивність анімацій та взаємодію з UI.
Недоліки:
requestIdleCallback
не гарантує виконання задачі в певний час.- Не працює в старих браузерах (Safari не підтримує).
3. Використання Web Workers (багатопотоковість)
Web Workers дозволяють виконувати задачі у фоновому потоці, повністю уникаючи блокування головного потоку.
Головний файл (main.js):
const worker = new Worker("worker.js");
worker.onmessage = function (e) {
console.log("Result from worker:", e.data);
};
worker.postMessage(1e9); // Надсилаємо велику кількість ітерацій
Файл Web Worker (worker.js):
self.onmessage = function (e) {
let sum = 0;
for (let i = 0; i < e.data; i++) {
sum += i;
}
self.postMessage(sum); // Повертаємо результат у головний потік
};
Переваги:
- Реальне багатопотокове виконання.
- Не блокує UI.
Недоліки:
- Web Worker не має доступу до DOM.
- Додаткові накладні витрати на передачу повідомлень.
4. Використання yield
у Generator
функціях
Генератори (function*
) дозволяють створювати “паузи” у виконанні функції, які можна контролювати.
Приклад:
function* longTaskGenerator() {
let sum = 0;
for (let i = 0; i < 1e9; i++) {
sum += i;
if (i % 1e6 === 0) yield; // "Перериваємося" на ітерації
}
return sum;
}
const task = longTaskGenerator();
function processNext() {
const result = task.next();
if (!result.done) {
setTimeout(processNext, 0); // Відкладаємо наступну ітерацію
} else {
console.log("Task completed", result.value);
}
}
processNext();
Переваги:
- Дозволяє виконувати довгі завдання поетапно.
- Гнучкий контроль над виконанням.
Недоліки:
- Виконання все ще однопотокове.
- Синтаксис може бути незручним для великих задач.
Висновок
У JavaScript є кілька підходів до розбиття тривалих завдань:
Метод | Блокування UI | Багатопоточність | Застосування |
---|---|---|---|
setTimeout / setInterval |
❌ Ні | ❌ Ні | Дрібні задачі, нескладна обробка |
requestIdleCallback |
❌ Ні | ❌ Ні | Фонові задачі без чітких дедлайнів |
Web Workers | ❌ Ні | ✅ Так | Тяжкі обчислення, незалежні задачі |
yield + Generator |
❌ Ні | ❌ Ні | Гнучке управління процесом |
Що вибрати?
- Якщо потрібно просто розбити задачу на частини без блокування UI — використовуйте
setTimeout
. - Якщо задача не термінова і може виконуватися, коли браузер “відпочиває” —
requestIdleCallback
. - Якщо потрібна реальна паралельність (обробка відео, складні обчислення) — Web Workers.
- Якщо треба контролювати процес поступового виконання — генератори (
yield
).
Головне — правильний вибір інструменту, адже неправильне використання може призвести до зайвих витрат ресурсів або складнощів у підтримці коду.
Сподіваюся, ця стаття допомогла вам краще розібратися у темі! 🚀