Ділення важкої задачі на частини
Чанкінг, це техніка, коли важке обчислення ріжуть на короткі шматки й між ними віддають керування 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
Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.