@absoluteui/services/scheduler
Idle and frame scheduling, chunked work that keeps the page responsive, and countdowns.
import { ... } from '@absoluteui/services/scheduler';How to use
Section titled “How to use”Keeps the main thread responsive while your code does real work: split long loops into time-sliced chunks, yield to input and rendering, run low-priority work when the browser is idle, and use timers that do not drift or overlap. Reach for it when a large import, a report calculation, or a list render makes the page freeze, or when you poll, count down a session, or animate. Every function falls back to plain timers on the server.
How do I process a large list without freezing the page?
Section titled “How do I process a large list without freezing the page?”runChunked maps items in slices of about budgetMs (default 8 ms) and yields between slices, so clicks and typing are handled promptly. Results keep their order; pass a signal to stop between items.
import { runChunked } from '@absoluteui/services/scheduler';
const employees = Array.from({ length: 20_000 }, (_, i) => ({ id: i + 1, salary: 50_000 + (i % 40) * 1_000 }));const controller = new AbortController();const withBonus = await runChunked(employees, (e) => ({ ...e, bonus: Math.round(e.salary * 0.08) }), { budgetMs: 8, signal: controller.signal });withBonus.length; // => 20000withBonus[0]; // => { id: 1, salary: 50000, bonus: 4000 }How do I yield inside my own loop?
Section titled “How do I yield inside my own loop?”Call await yieldToMain() every so often. It uses scheduler.yield() where available, so your work resumes before other queued tasks.
import { yieldToMain } from '@absoluteui/services/scheduler';
const list = document.createElement('ul');const names = Array.from({ length: 2_000 }, (_, i) => `Employee ${String(i + 1)}`);let last = performance.now();for (const name of names) { list.append(Object.assign(document.createElement('li'), { textContent: name })); if (performance.now() - last > 8) { await yieldToMain(); last = performance.now(); }}list.childElementCount; // => 2000For thousands of rows, prefer a virtualized grid over rendering every row.
How do I queue small tasks in order?
Section titled “How do I queue small tasks in order?”createTaskScheduler runs tasks one after another and yields whenever the budget is spent. Each schedule call resolves with that task’s result.
import { createTaskScheduler } from '@absoluteui/services/scheduler';
const tasks = createTaskScheduler({ budgetMs: 8 });const widgets = ['headcount', 'attrition', 'open-positions'];const hydrated = await Promise.all(widgets.map((w) => tasks.schedule(() => `${w} ready`)));hydrated; // => ['headcount ready', 'attrition ready', 'open-positions ready']tasks.pending; // => 0How do I run work when the browser is idle?
Section titled “How do I run work when the browser is idle?”scheduleIdle suits analytics, prefetching, or cleaning caches. Check timeRemaining() and stop when it runs low; the timeout guarantees the work runs eventually.
import { scheduleIdle } from '@absoluteui/services/scheduler';
const auditEvents = ['viewed:42', 'viewed:43', 'exported:payroll'];const sent: string[] = [];const handle = scheduleIdle((deadline) => { while (deadline.timeRemaining() > 1 && auditEvents.length > 0) sent.push(auditEvents.shift() ?? '');}, { timeout: 2_000 });// handle.cancel() if the page is torn down firstHow do I poll every 30 seconds without overlapping requests?
Section titled “How do I poll every 30 seconds without overlapping requests?”interval stays aligned to the start time, never starts a tick while an async callback is still running, and can pause while the tab is hidden.
import { interval } from '@absoluteui/services/scheduler';
const controller = new AbortController();interval(async () => { const r = await fetch('/api/approvals/pending', { signal: controller.signal }); if (r.ok) console.info('Pending approvals', await r.json());}, 30_000, { immediate: true, pauseWhenHidden: true, signal: controller.signal, onError: (e) => { console.warn(e); } });// controller.abort() stops polling when the user leaves the pageHow do I warn before a session times out?
Section titled “How do I warn before a session times out?”countdown counts to an absolute deadline and is recomputed from the clock on every tick, so throttled background tabs do not drift. reset extends it when the user chooses to stay signed in (WCAG 2.2.1).
import { countdown } from '@absoluteui/services/scheduler';
const warning = document.createElement('p');warning.setAttribute('role', 'status');const session = countdown(Date.now() + 15 * 60_000, (ms) => { if (ms <= 120_000 && ms % 60_000 < 1_000) warning.textContent = `You will be signed out in ${String(Math.ceil(ms / 60_000))} minutes.`;}, { onComplete: () => { location.assign('/signed-out'); } });const stay = document.createElement('button');stay.textContent = 'Stay signed in';stay.addEventListener('click', () => { session.reset(Date.now() + 15 * 60_000); });What goes wrong?
Section titled “What goes wrong?”- Chunking keeps the page responsive but does not make work faster; move heavy CPU work (large parses, crypto on big files) to a Web Worker.
- Invalid timings throw:
budgetMsmust be positive (SVC_SCHEDULER_INVALID_BUDGET) and interval periods at least 1 ms (SVC_SCHEDULER_INVALID_MS).
import { interval, runChunked } from '@absoluteui/services/scheduler';
await runChunked([1, 2, 3], (n) => n * 2, { budgetMs: 0 }); // => throws SVC_SCHEDULER_INVALID_BUDGETinterval(() => undefined, 0); // => throws SVC_SCHEDULER_INVALID_MSAPI reference
Section titled “API reference”Functions
Section titled “Functions”countdown
Counts down to an absolute deadline (epoch ms), calling onTick(remainingMs) aligned to intervalMs.
Computed from the clock on every tick, so throttled background timers never make it drift.
Supports WCAG 2.2.1 Timing Adjustable: warn before a session expires and let users extend it via reset.
Announce the remaining time sparingly (for example each minute) in a polite live region.
function countdown(deadline: number, onTick: (remainingMs: number) => void, options?: CountdownOptions): CountdownHandle;Example:
const sessionExpiresAt = Date.now() + 15 * 60_000;const warning = document.createElement('p');const c = countdown(sessionExpiresAt, (ms) => { if (ms <= 120_000) warning.textContent = `Signing out in ${Math.ceil(ms / 1000)} s`; }, { onComplete: () => location.assign('/signed-out'),});const stay = document.createElement('button');stay.onclick = () => c.reset(Date.now() + 15 * 60_000);createTaskScheduler
Serial queue of small main-thread tasks that yields to input and rendering when the time budget is spent.
function createTaskScheduler(options?: { readonly budgetMs?: number;}): TaskScheduler;Example:
const tasks = createTaskScheduler({ budgetMs: 8 });const widgets = [{ hydrate: () => 'chart' }, { hydrate: () => 'grid' }];for (const w of widgets) void tasks.schedule(() => w.hydrate());interval
Drift-corrected repeating timer: ticks stay aligned to start + n x ms instead of accumulating
setTimeout lateness. Async callbacks never overlap (missed ticks are skipped).
function interval(fn: () => unknown, ms: number, options?: IntervalOptions): TaskHandle;Example:
const controller = new AbortController();const h = interval(() => fetch('/api/notifications'), 30_000, { pauseWhenHidden: true, signal: controller.signal });controller.abort(); // stops pollingrunChunked
Processes items in time-sliced chunks, yielding to the main thread whenever budgetMs is used up, so
large loops do not cause long tasks (better INP). Resolves the mapped results in order.
function runChunked<T, R>(items: Iterable<T>, fn: (item: T, index: number) => R, options?: RunChunkedOptions): Promise<R[]>;Example:
const records = [{ id: 1, name: 'Ada' }, { id: 2, name: 'Grace' }];const rows = await runChunked(records, (r) => `${r.id}: ${r.name}`, { budgetMs: 8 });rows; // => ['1: Ada', '2: Grace']scheduleFrame
Runs fn before the next paint (requestAnimationFrame), or after about 16 ms where unsupported.
function scheduleFrame(fn: (time: number) => void): TaskHandle;Example:
const panel = document.createElement('div');scheduleFrame(() => panel.classList.add('open'));scheduleIdle
Runs fn when the browser is idle (requestIdleCallback), or after a short timeout where unsupported.
function scheduleIdle(fn: (deadline: IdleDeadlineLike) => void, options?: { readonly timeout?: number;}): TaskHandle;Example:
const queue = ['audit-1', 'audit-2', 'audit-3'];const sent: string[] = [];scheduleIdle((d) => { while (d.timeRemaining() > 1 && queue.length > 0) sent.push(queue.shift() ?? ''); }, { timeout: 2000 });yieldToMain
Yields to the event loop so input and rendering can run: scheduler.yield() when present (keeps priority),
else a MessageChannel macrotask, else setTimeout(0).
function yieldToMain(): Promise<void>;Example:
const rows = Array.from({ length: 1000 }, (_, i) => `Row ${i + 1}`);const list = document.createElement('ul');for (const [i, row] of rows.entries()) { list.append(Object.assign(document.createElement('li'), { textContent: row })); if ((i + 1) % 100 === 0) await yieldToMain();}Interfaces
Section titled “Interfaces”CountdownHandle
Handle returned by {@link countdown}.
interface CountdownHandle extends TaskHandle { readonly remaining: number; reset(deadline: number): void;}CountdownOptions
Options for {@link countdown}.
interface CountdownOptions { readonly signal?: AbortSignal; readonly intervalMs?: number; readonly onComplete?: () => void; readonly now?: () => number;}IdleDeadlineLike
Deadline passed to idle callbacks.
interface IdleDeadlineLike { readonly didTimeout: boolean; timeRemaining(): number;}IntervalOptions
Options for {@link interval}.
interface IntervalOptions { readonly signal?: AbortSignal; readonly immediate?: boolean; readonly pauseWhenHidden?: boolean; readonly onError?: (error: unknown) => void;}RunChunkedOptions
Options for {@link runChunked}.
interface RunChunkedOptions { readonly budgetMs?: number; readonly signal?: AbortSignal;}TaskHandle
Cancellable scheduled callback.
interface TaskHandle { cancel(): void;}TaskScheduler
Scheduler returned by {@link createTaskScheduler}.
interface TaskScheduler { schedule<R>(task: () => R | Promise<R>): Promise<Awaited<R>>; runChunked<T, R>(items: Iterable<T>, fn: (item: T, index: number) => R, options?: { readonly signal?: AbortSignal; }): Promise<R[]>; readonly pending: number; clear(): void;}