skip to content

You lead a large web app where teams keep deferring work — prefetch, analytics, offscreen widget setup. How would you set a main-thread scheduling policy, and what goes wrong if everything is pushed into idle callbacks or background priority?

level: principalimportance: nice to knowfreq 22%

answer

  1. two questions, not a list of APIs
  2. who is blocked on this?
  3. how wrong is it if it is late?
  4. labels only rank things against each other
  5. deferring moves cost, never removes it

basics

~20 s

Classify deferrable work by who waits for it and how stale it may become, then map each class to a primitive with a bound: urgent to an explicit high priority, deferrable to a background task, disposable to an idle callback with a timeout. Deferring everything just relocates the jam.

solid answer

~60 s

A scheduling policy is a classification exercise, not a choice of API. I would have teams answer two questions per unit of work: who is waiting for it, and how stale may it become before it is wrong or useless? That yields a small number of classes — user-blocking, visible-but-not-awaited, deferrable-with-a-deadline, and genuinely disposable — and each maps to a primitive: an explicit high priority, the default, a background task or an idle callback *with a timeout*, and an idle callback without one. The failure mode of "defer everything" is that priority stops carrying information: when every team marks its own work background, the queue is again undifferentiated, just later. The second failure is that deferral never reduces total main-thread cost — it moves the same work to a moment you have not measured, often the first interaction. And a page that never goes idle starves the disposable tier permanently, which is why the important rule is that anything with a correctness deadline gets an explicit bound rather than hoping for leftover time.

go deeper

for a junior

Know that deferring work does not make it cheaper — it moves the same cost to a later moment, which may be a worse one, and some deferred callbacks may never run at all.

for a middle

Be able to match each primitive to a class of work: explicit priority for what a user waits on, background or a timed idle callback for work with a deadline, an unbounded idle callback only for work that may never run.

for a senior

Expect to name the concrete failure modes — priority inflation, relocated jank landing during the first interaction, starved idle callbacks — and to diagnose which one a given page is suffering.

for a principal

Own the classification itself: who decides a work item's class, where the primitives are centralised so the choice is reviewable, what the default is, and how this policy is reviewed against the responsiveness metrics your performance owners hold.

## The problem a policy solves When many teams share one main thread, each of them locally reasons "my work is not urgent, I will defer it", and each is individually right. Collectively they produce a page where the deferred pile is as contended as the urgent one, and where the moment it lands — usually just after first paint, when everything's deferral timer expires at once — is worse than if the work had been done eagerly and spread out. A policy exists to make those local decisions add up. ## Classify by waiting and staleness The two questions that determine everything: **Who is waiting?** If a human is blocked on the result, it is not deferrable at any price; deferring it converts a CPU cost into a perceived delay, which is worse. If nothing on screen depends on it, it can wait. **How stale may it become?** This is the question teams skip, and it is the one that picks the primitive. Work that is merely nice to have late — prefetching a route the user may visit — can run in leftover time and be dropped entirely. Work that is wrong if it is late — a batched write, a consent record, a session heartbeat — needs a bound, which means a timeout or an explicit deadline, not an unbounded idle request. Four classes cover most codebases: | class | who waits | primitive | | --- | --- | --- | | user-blocking | the user, right now | explicit high priority; usually not deferred at all | | visible, not awaited | the user, soon | default priority | | deferrable with a deadline | a system, eventually | background priority, or an idle callback with a timeout | | disposable | nobody | idle callback, no timeout, may never run | The useful discipline is that a team must be able to say which row their work is in, and the fourth row must be honest: if the code breaks when the callback never runs, it is not disposable. ## The failure modes of over-deferring **Priority inflation and deflation.** Labels only order work relative to each other. If every team marks its work background, the background queue is the queue, and you have re-created an undifferentiated FIFO one tier down. The same happens in the other direction: if three teams each decide their work is user-blocking, none of them is. **Deferral is not reduction.** Moving 400 ms of parsing out of load does not delete it. It arrives later, usually unmeasured, and frequently during the first interaction — which is exactly when it hurts most. Deferring is a legitimate answer only when the later moment is genuinely cheaper; otherwise the answer is to do less work, do it off-thread, or not do it. **Starvation of the disposable tier.** A page whose main thread never goes idle never runs unbounded idle callbacks. That is fine by definition for genuinely disposable work, and fatal for work misfiled as disposable. Persistent starvation is also a signal in its own right: it means the thread has no headroom, and the scheduling policy is treating a symptom. **Chunking without yielding correctly.** A deferred job is still a main-thread job. A team that defers a long task without splitting it has only changed when the page freezes. ## Making the policy survive contact Centralise the primitives in one small module — one `postTask`-style entry point with a required class argument, one yielding helper, one feature-detection branch — so the classification is a visible argument at every call site rather than a scattered choice of API. That gives you something to review in a diff and something to grep for during an incident, and it means the day a new primitive becomes universally available you change one file. Set a default. In most apps the correct default is *not* deferred: work runs at ordinary priority unless someone has argued it into another class. Defaults that favour deferral quietly accumulate a second, invisible workload. Finally, keep the boundary with your performance discipline clear. The scheduling policy decides *when* main-thread work runs and with what guarantee; whether the resulting responsiveness is acceptable is measured by the metrics your performance owners track, and a policy that looks tidy but moves the jam is caught there, not here. The two have to be reviewed together, or you will optimise the shape of the queue while the user's experience is unchanged.

  • How do you stop every team from classifying its own work as urgent?
    Make the class an explicit, reviewable argument at the call site rather than an API choice buried in a module, and require a stated reason for anything above the default. Then audit: if the high-priority tier is more than a small fraction of scheduled work, the labels have stopped ranking anything. Priority is a shared, zero-sum resource, so it needs an owner rather than a convention.
  • When is the right answer not to defer at all?
    When the later moment is not actually cheaper — which is most of the time on a page that stays busy. If the work is expensive and always needed, deferring only relocates the freeze, often into the first interaction where it is most visible. The alternatives are to do less work, move it off the main thread, or accept the cost during load when the user already expects to wait.
  • What is the tell that a scheduling policy is masking a real capacity problem?
    That the page never produces idle periods. If unbounded idle callbacks are permanently starved, the main thread has no headroom at all, and every deferred subsystem is queuing behind the same congestion. At that point rearranging priorities is triage; the finding is that the page's total main-thread work exceeds what the device can absorb.
  • How would you handle deferrable work that must not be lost if the user navigates away?
    Do not let scheduling be the durability mechanism. A pending idle callback or posted task is discarded when the page goes away, so anything that must survive needs a delivery path designed for that, plus local durability so it can be retried on the next visit. The scheduler decides when to try, never whether the data is safe.

saying these in an interview costs you the question

  • Treats deferring work as if it reduced total main-thread cost
  • Marks all non-render work as background priority
  • Uses an unbounded idle callback for work that has a deadline
  • Sets deferral as the default and never revisits it
  • Assumes pending scheduled work survives a navigation

context