Microtask Batch SchedulerLoading saved progress…

Microtask Batch Scheduler

A microtask batch scheduler collects every item handed to it during one synchronous run of your code and delivers them together in a single callback on the next microtask, instead of reacting to each item the moment it arrives. Many independent parts of an app can each say "this changed, please react" within the same tick — a dozen store writes, a burst of DOM mutations, several dirty components. Firing your expensive reaction (a re-render, a network sync, a recompute) once per call is wasteful; you want to wait until the burst is over and react once with the full list. The earliest safe moment to do that is the next microtask — the gap right after your synchronous code returns control to the event loop.

Implement microtaskBatchScheduler(onFlush), which returns an object with a single schedule method.

Signature

function microtaskBatchScheduler<T>(
  onFlush: (items: T[]) => void,
): {
  schedule: (item: T) => void;
};

Examples

const scheduler = microtaskBatchScheduler((items) => {
  console.log('flush', items);
});

scheduler.schedule('a');
scheduler.schedule('b');
scheduler.schedule('c');
// nothing logged yet — still inside the same synchronous frame

await Promise.resolve(); // let the microtask run
// → flush ['a', 'b', 'c']   (one call, all three items, in order)
// A schedule in a later frame is a brand-new batch.
scheduler.schedule('x');
await Promise.resolve(); // → flush ['x']

scheduler.schedule('y');
await Promise.resolve(); // → flush ['y']   (a second, separate flush)

Notes

  • One flush per frame — no matter how many times you call schedule in a single synchronous run, onFlush fires exactly once, with every item from that run.
  • Next microtask, not later — schedule the flush with queueMicrotask (or Promise.resolve().then) so it lands before the next timer, render, or setTimeout(…, 0).
  • Order is preserved — items reach onFlush in the order they were scheduled; you never sort or reorder.
  • No de-duplication — if the same value is scheduled twice, onFlush receives it twice. Do not filter.
  • A new frame starts a new batch — once the flush has run, the next schedule begins a fresh batch and queues a fresh flush.
  • No real timers — this is pure microtask plumbing; you never need setTimeout.

FAQ

Why flush on a microtask instead of setTimeout?
A microtask runs as soon as the current synchronous call stack unwinds — before any timer callback and before the browser paints. A setTimeout(…, 0) is a macrotask that fires after all microtasks and possibly after a render, so the batch would arrive later than it needs to.
What counts as one 'frame', and when does a new batch start?
A frame is one synchronous run of your code up to the point control returns to the event loop. Every schedule call in that run joins the same batch; once the microtask flush empties it, the very next schedule opens a fresh batch and queues a new flush.
What are the time and space complexities?
schedule is O(1) — a push plus a flag check — and the flush is O(n) in the batch size for the single onFlush call. Space is O(n) for the n items buffered during the current frame.
Loading editor…