Skip to main content

Splitting a heavy task into chunks

Chunking is the technique of cutting a heavy computation into short slices and handing control back to the event loop between them through a macrotask. That way the browser gets to paint a frame and process input, and a Node.js server gets to serve other requests.

Theory

TL;DR

  • Do a small slice of work, roughly 1 to 2 ms.
  • Schedule the continuation as a macrotask, so the browser or Node can handle UI, requests and timers.
  • Repeat until the work is finished.
  • await Promise.resolve() does not help: it is a microtask and it does not yield a frame.
  • Macrotasks: setTimeout(fn, 0), setImmediate, MessageChannel, requestAnimationFrame, requestIdleCallback.
  • Very heavy math or parsing is better moved into a Web Worker or worker_threads than chunked by hand.

Quick example

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(); })(); }); }

The principle

  • Do a small slice of work (for example, 1 to 2 ms).
  • Schedule the continuation through a macrotask (so the browser or Node can process UI, requests, timers).
  • Repeat until it is done.

Important: await Promise.resolve() does not help, it is a microtask and it does not yield the frame or the UI. You need a macrotask: setTimeout(0), setImmediate, MessageChannel, requestAnimationFrame, requestIdleCallback.

A general purpose chunking utility

In the browser (UI friendly, 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 (so the server does not hang)

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; }

Quick recipes

1) Chunking by operation count (simple and reliable)

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) With requestAnimationFrame (smooth 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) Maximum gentleness, requestIdleCallback (where available)

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) The fastest macrotask scheduler, 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();

Which one to pick

  • Animations and interactive UI: requestAnimationFrame with a budget of 2 to 5 ms.
  • Low priority, background tasks: requestIdleCallback (with a setTimeout fallback).
  • Simple and available everywhere: setTimeout(0) or MessageChannel.
  • Node.js: setImmediate, or slicing by iteration count.
  • Very heavy math or parsing: better moved into a Web Worker (browser) or worker_threads (Node), so you do not have to slice it by hand.

Common mistakes

Antipatterns to avoid:

  • Infinite or long synchronous loops with no yielding (while (true) {} or a for over billions of iterations).
  • Hoping that await Promise.resolve() relieves the UI, it is a microtask and no frame is painted.
  • Synchronous I/O and crypto in Node.js (readFileSync, pbkdf2Sync), they block the whole process.
  • Slices that are too small: scheduling a separate macrotask per element means the scheduler overhead eats the entire gain and the job stretches into minutes.
  • requestIdleCallback with no timeout and no fallback: on a busy page the task may never run, and in some engines the API simply does not exist.
  • Reading DOM sizes or styles inside a chunk: that forces a synchronous reflow and undoes the frame budget you were saving.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.