Способи поділу тривалих завдань у JavaScript

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).

Головне — правильний вибір інструменту, адже неправильне використання може призвести до зайвих витрат ресурсів або складнощів у підтримці коду.

Сподіваюся, ця стаття допомогла вам краще розібратися у темі! 🚀