Skip to main content

Ділення важкої задачі на частини

Чанкінг, це техніка, коли важке обчислення ріжуть на короткі шматки й між ними віддають керування event loop через макрозадачу. Завдяки цьому браузер встигає перемалювати кадр і обробити ввід, а сервер Node.js, обслужити інші запити.

Теорія

TL;DR

  • Робимо невеликий шматок роботи, приблизно 1-2 ms.
  • Плануємо продовження через макрозадачу, щоб браузер або Node встигли обробити UI, запити й таймери.
  • Повторюємо, доки не закінчимо.
  • await Promise.resolve() не допомагає: це мікрозадача, вона не віддає кадр.
  • Макрозадачі: setTimeout(fn, 0), setImmediate, MessageChannel, requestAnimationFrame, requestIdleCallback.
  • Дуже важку математику чи парсинг краще винести у Web Worker або worker_threads, а не ділити руками.

Швидкий приклад

javascript
// Chunking by operation count: simple and reliable function forEachChunked(arr, fn, { chunkSize = 10_000, schedule = setTimeout } = {}) { return new Promise((resolve) => { let i = 0; (function loop() { const end = Math.min(i + chunkSize, arr.length); for (; i < end; i++) fn(arr[i], i); i < arr.length ? schedule(loop, 0) : resolve(); })(); }); }

Принцип

  • Робимо невеликий шматок роботи (наприклад, 1-2 ms).
  • Плануємо продовження через макрозадачу (щоб браузер або Node змогли обробити UI, запити, таймери).
  • Повторюємо, доки не закінчимо.

Важливо: await Promise.resolve() не допомагає, це мікрозадача, вона не віддає кадр і UI. Потрібна макрозадача: setTimeout(0), setImmediate, MessageChannel, requestAnimationFrame, requestIdleCallback.

Універсальна утиліта чанкінгу

У браузері (дружня до UI, 60fps)

javascript
// Schedulers for different modes: const scheduleTimeout = (fn) => setTimeout(fn, 0); // simple const scheduleRAF = (fn) => requestAnimationFrame(fn); // gentle on the UI const scheduleIdle = (fn) => requestIdleCallback(fn, { timeout: 50 }); // when the browser is idle (a fallback is mandatory) // Time based chunker: works for about N milliseconds, then yields the loop export function runSliced(work, { budgetMs = 4, schedule = scheduleRAF } = {}) { return new Promise((resolve, reject) => { try { const step = (deadlineLike) => { const start = performance.now(); while (work.hasMore()) { work.doOne(); const now = performance.now(); // Over budget, so yield the frame if (now - start >= budgetMs) { schedule(step); return; } } resolve(); }; schedule(step); } catch (e) { reject(e); } }); } // Usage example: processing a large array export async function processBigArray(items) { let i = 0; const work = { hasMore: () => i < items.length, doOne: () => { const x = items[i]; // ...heavy work on x... i++; } }; await runSliced(work, { budgetMs: 3, schedule: scheduleRAF }); }

Node.js (сервер не «висне»)

javascript
const scheduleImmediate = (fn) => setImmediate(fn); function runSlicedNode(work, { iterationsPerSlice = 50_000, schedule = scheduleImmediate } = {}) { return new Promise((resolve, reject) => { try { const step = () => { let n = iterationsPerSlice; while (work.hasMore() && n--) { work.doOne(); } work.hasMore() ? schedule(step) : resolve(); }; schedule(step); } catch (e) { reject(e); } }); } // Example: async function crunch() { let i = 0, N = 1e9, sum = 0; const work = { hasMore: () => i < N, doOne: () => { sum += i % 7; i++; } }; await runSlicedNode(work, { iterationsPerSlice: 100_000 }); return sum; }

Швидкі рецепти

1) Чанкінг за кількістю операцій (просто й надійно)

javascript
function forEachChunked(arr, fn, { chunkSize = 10_000, schedule = setTimeout } = {}) { return new Promise((resolve) => { let i = 0; (function loop() { const end = Math.min(i + chunkSize, arr.length); for (; i < end; i++) fn(arr[i], i); i < arr.length ? schedule(loop, 0) : resolve(); })(); }); }

2) Через requestAnimationFrame (плавний UI)

javascript
function timeSliced(fn, { budgetMs = 4 } = {}) { return new Promise((resolve) => { function tick() { const start = performance.now(); let more = true; while (more && performance.now() - start < budgetMs) { more = fn(); // fn() does a little work and returns true if something is left } more ? requestAnimationFrame(tick) : resolve(); } requestAnimationFrame(tick); }); }

3) Максимальна обережність, requestIdleCallback (якщо доступний)

javascript
function idleSliced(fn, timeout = 50) { return new Promise((resolve) => { function step(deadline) { let more = true; while (more && (deadline.timeRemaining() > 1)) { more = fn(); } more ? requestIdleCallback(step, { timeout }) : resolve(); } requestIdleCallback(step, { timeout }); }); }

4) Найшвидший планувальник макрозадач, MessageChannel

javascript
function macroTaskScheduler() { const ch = new MessageChannel(); let queue = []; ch.port1.onmessage = () => queue.shift()?.(); return (fn) => { queue.push(fn); ch.port2.postMessage(0); }; } const scheduleFast = macroTaskScheduler();

Що обрати

  • Анімації та інтерактивний UI: requestAnimationFrame з бюджетом 2-5 ms.
  • Низький пріоритет, фонові задачі: requestIdleCallback (і fallback на setTimeout).
  • Просто і всюди: setTimeout(0) або MessageChannel.
  • Node.js: setImmediate або розбивка за кількістю ітерацій.
  • Дуже важка математика чи парсинг: краще винести у Web Worker (браузер) або worker_threads (Node), щоб не ділити вручну.

Типові помилки

Антипатерни, яких варто уникати:

  • Нескінченні або довгі синхронні цикли без «поступки» (while (true) {} чи for на мільярди ітерацій).
  • Надія на await Promise.resolve() для розвантаження UI, це мікрозадача, кадр не перемалюється.
  • Синхронні версії вводу-виводу й криптографії в Node.js (readFileSync, pbkdf2Sync), вони блокують увесь процес.
  • Занадто малий чанк: якщо на кожен елемент планувати окрему макрозадачу, накладні витрати планувальника з'їдять увесь виграш, а робота розтягнеться на хвилини.
  • requestIdleCallback без timeout і без fallback: на завантаженій сторінці задача може не запуститися взагалі, а в частині рушіїв API просто відсутній.
  • Читання розмірів або стилів DOM усередині чанка: це змушує браузер робити примусовий reflow і зводить нанівець економію бюджету кадру.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.