Representative interview topic

How do you use scheduler.postTask safely with a progressive fallback?

FrontendHard
Offer.cc Editorial TeamPublished Updated

Question

A web app must process a search index, visible rendering, and low-priority prefetching on the main thread. Design a scheduler with scheduler.postTask and explain priorities, cancellation, scheduler.yield, unsupported browsers, and how to prove interaction remains responsive.

1. Prompt

A page updates search results as the user types while parsing an offline index and prefetching the next page. Design a task layer using the Prioritized Task Scheduling API: visible updates should run promptly without background prefetch blocking input. Explain scheduler.postTask(), scheduler.yield(), cancellation signals, and behavior when the API is unavailable.

2. Constraints and clarifications

  • All tasks run in one window or Worker event loop; a long synchronous function still blocks that thread.
  • Classify work into at least user-blocking, user-visible, and background priorities.
  • Scrolling, route changes, or new input can make work obsolete, so it must be cancellable or reprioritizable.
  • Keep a working fallback; business correctness cannot depend on browser support.

3. Core approach

scheduler.postTask(callback, options) queues a callback with a priority and returns a Promise. priority can be user-blocking, user-visible, or background. Pass an AbortSignal to cancel; a shared TaskController can also change the priority of a task that has not started. scheduler.yield() lets an async function voluntarily return control to the browser before continuing.

Priority changes ordering; it does not preempt JavaScript that is already running. Keep each callback short and yield between chunks. When the API is unavailable, use small setTimeout or MessageChannel batches, or an existing framework scheduler, while preserving the same cancellation and stale-result semantics.

4. Reference implementation

javascript
const scheduler = globalThis.scheduler;

function scheduleWork(task, priority, signal) {
  if (scheduler?.postTask) {
    return scheduler.postTask(task, { priority, signal });
  }

  return new Promise((resolve, reject) => {
    const run = () => {
      if (signal?.aborted) {
        reject(signal.reason);
        return;
      }
      Promise.resolve().then(task).then(resolve, reject);
    };
    setTimeout(run, priority === "background" ? 50 : 0);
  });
}

async function indexInChunks(items, signal) {
  for (let i = 0; i < items.length; i += 100) {
    await scheduleWork(() => buildIndex(items.slice(i, i + 100)),
      "background", signal);
    if (scheduler?.yield && i + 100 < items.length) {
      await scheduler.yield({ signal });
    }
  }
}

5. Performance and correctness

Priority does not change program results or interrupt a running callback; it only affects the relative order of tasks that have not started. The Promise from scheduler.postTask() settles with the callback’s return value, while callback errors or cancellation should be handled as rejections by the caller.

The real performance boundary is task duration and total work: a 200 ms synchronous loop still blocks input when placed in a low-priority queue. Measure Long Tasks, input delay, and cancellation hit rate per batch, then tune the chunk size. CPU-heavy work that can run in parallel may belong in a Worker; priority alone cannot hide main-thread starvation.

6. Follow-ups and traps

  • Detect globalThis.scheduler?.postTask instead of guessing support from browser brand or version.
  • An AbortSignal cancels work that has not started or that observes the signal; it cannot forcibly stop a synchronous callback already running.
  • Dynamic priority is not preemption. If a task has already left the queue, use a business-level version check to avoid committing stale results.
  • A setTimeout fallback cannot reproduce all native priority semantics, so verify visible work, background work, and cancellation paths explicitly.

7. Further reading

Compare scheduler.postTask() with requestIdleCallback(), MessageChannel, and framework schedulers: the first provides explicit priority and cancellation, idle callbacks depend on idle opportunities, message channels only queue work, and frameworks may add component-lifecycle semantics. Choose using compatibility, task type, and measured behavior.

8. Interview scoring points

Can explain priority boundaries

The candidate should cover the three priorities, the returned Promise, and the fact that only not-yet-started tasks are ordered; it is not thread preemption.

Can design cancellable work

They should use AbortSignal or TaskController, explain that a started callback cannot be forcibly stopped, and add stale-result version checks.

Can write a progressive fallback

They should feature-detect first, then provide timer, message-channel, or framework scheduling while preserving task classes and cancellation behavior.

Can prove the benefit with data

They should measure Long Tasks, input delay, chunk duration, and cancellation hit rate, and recognize that Workers isolate truly CPU-heavy work.

Public sources

Related questions