skip to content

In a browser, what object is passed to the callback you register with requestIdleCallback(), and what do its timeRemaining() and didTimeout members tell you?

level: juniorimportance: should knowfreq 45%

answer

  1. one argument, not a timestamp
  2. it is a method call, not a property
  3. there is a hard ceiling on the value
  4. a flag for "ran anyway, not really idle"
  5. 50 ms and didTimeout

basics

~20 s

requestIdleCallback invokes your callback with an IdleDeadline object. timeRemaining() returns how many milliseconds of idle time are left in this period, capped at 50, and didTimeout is true when the callback ran only because the timeout option expired.

solid answer

~50 s

The callback receives a single `IdleDeadline` argument. `deadline.timeRemaining()` is a method, not a property: it returns a fresh estimate, in milliseconds, of how much idle time is left before the browser needs the main thread again, and it counts down toward zero as you work. It never starts above 50 ms, so an idle callback is a slot for small units of work, not a place to run a long job. `deadline.didTimeout` is a boolean that is true only when the callback was forced to run because the `timeout` you passed in the options object elapsed before any idle period appeared — in that case `timeRemaining()` is effectively 0 and anything you do runs at the cost of responsiveness. The idiomatic shape is a `while` loop guarded by `deadline.timeRemaining() > someThreshold`, which re-schedules itself with another `requestIdleCallback` when work is left over.

code

javascript · 15 lines
javascript
const queue = Array.from({ length: 5000 }, (_, i) => i);
let sum = 0;

function drain(deadline) {
  while (queue.length > 0 && (deadline.timeRemaining() > 1 || deadline.didTimeout)) {
    sum += queue.shift();
  }
  if (queue.length > 0) {
    requestIdleCallback(drain);
  } else {
    console.log('done', sum);
  }
}

requestIdleCallback(drain);

go deeper

for a junior

Know that the callback gets one argument, an IdleDeadline, that timeRemaining() is a method you call each iteration, and that cancelIdleCallback with the returned handle is how you clean up.

for a middle

Be able to write the drain loop from memory: check the deadline before each unit, use a small margin rather than zero, treat didTimeout as permission to work anyway, and re-schedule while items remain.

for a senior

Expect to explain that the deadline is advisory and nothing preempts a running callback, so the real safety property comes from keeping each unit of work small and uniform rather than from trusting the number.

for a principal

Own the guidance for when deferred work is legitimate at all: which work may be dropped or arrive minutes late without a correctness problem, and how teams feature-detect and fall back where the API is missing.

## What an idle period is The browser's main thread spends a turn running tasks — an input handler, a timer callback, a network response — and then, when a frame is due, running rendering steps. If the thread finishes everything it owes before the next frame is needed, the leftover time is an **idle period**. `requestIdleCallback(callback)` asks the browser to hand you one of those leftovers. That is the whole idea: idle callbacks are opportunistic. They are not a timer and not a priority queue. You are asking to be woken up *if and when* the browser has nothing better to do. ## The IdleDeadline argument When your callback runs, the browser passes exactly one argument, an object implementing the `IdleDeadline` interface: ```js requestIdleCallback((deadline) => { console.log(deadline.timeRemaining(), deadline.didTimeout); }); ``` - **`timeRemaining()`** — a *method* returning a `DOMHighResTimeStamp` (milliseconds, fractional). It is a live estimate: call it again a moment later and it has gone down. It reaches 0 when the browser wants the thread back. Its value never exceeds **50 ms**, even in a completely idle tab, because 50 ms is the threshold beyond which a task starts to feel like a dropped input to a user. - **`didTimeout`** — a read-only boolean. It is `false` for a normal idle-time invocation and `true` when the browser ran your callback only because the `timeout` you supplied expired. A common bug is writing `deadline.timeRemaining > 0`, which compares a *function object* to zero and is always true. The parentheses matter. ## The options object and cancellation `requestIdleCallback(callback, options)` accepts one option, `timeout`, in milliseconds: ```js const handle = requestIdleCallback(flush, { timeout: 2000 }); cancelIdleCallback(handle); ``` The return value is an integer handle, and `cancelIdleCallback(handle)` cancels a pending callback — the direct analogue of `clearTimeout`. There is no `clearIdleCallback`. Cancelling matters in component teardown: an idle callback that fires after the widget that scheduled it is gone will touch detached state or dead DOM. ## The chunking idiom Because the deadline is small and unpredictable, the standard pattern is a drain loop that checks the deadline between units and re-schedules itself: ```js function drain(deadline) { while (queue.length && (deadline.timeRemaining() > 1 || deadline.didTimeout)) { handle(queue.shift()); } if (queue.length) requestIdleCallback(drain); } ``` Three things are load-bearing here. First, the check happens **before** each unit, not after, so you never start an item you have no budget for. Second, the guard uses a small margin (`> 1`) rather than `> 0`, because the check itself costs time and one more item may overshoot. Third, the loop must assume the unit of work is *small and roughly uniform*; the deadline tells you how much time is left, not how long your next item takes, so a single item that runs for 30 ms will blow through a 4 ms deadline and the browser cannot stop you. The deadline is advisory — nothing preempts a running callback. Including `|| deadline.didTimeout` makes the timed-out invocation still do work; without it, a timed-out callback would see `timeRemaining()` of 0, do nothing, and re-schedule forever. ## What it is not for Idle callbacks are wrong for anything the user is waiting on, anything that must run at a predictable moment, and anything that mutates the DOM in a way the user would see mid-frame — visual work belongs on the frame callback path, not here, because an idle callback can land after style and layout have already been computed for the frame and force the browser to redo them. ## Availability `requestIdleCallback` has been in Chromium and Firefox for years; Safari lacked it for a long time and support there is comparatively recent, which is why libraries commonly feature-detect and fall back to a timer-based scheduler. Check `typeof requestIdleCallback === 'function'` rather than assuming it exists, and note that it is exposed on `window` — dedicated workers do not get it, so worker-side chunking has to use a different mechanism.

  • Why does timeRemaining() cap out at 50 ms rather than reporting the full idle stretch in a quiet tab?
    Because 50 ms is roughly the point at which an unresponsive main thread becomes perceptible: if input arrives just after your callback starts, the browser wants to be back in control within about that long. Capping the reported budget stops callers from starting a 200 ms unit of work on the strength of a genuinely quiet moment that a tap could end at any instant.
  • What happens if the work you do inside an idle callback overruns the deadline you were given?
    Nothing stops you — the deadline is advisory, and a callback runs to completion like any other JavaScript task. `timeRemaining()` simply reports 0 and the browser's next frame or input handling is delayed by however long you overran. That is why the guard belongs before each unit of work, and why units must be small enough that one of them cannot itself be a long task.
  • How do you cancel a scheduled idle callback, and when does forgetting to matter?
    Keep the integer handle that `requestIdleCallback` returns and pass it to `cancelIdleCallback(handle)`. It matters on teardown: a component that schedules deferred work and then unmounts leaves a callback that will still fire, typically minutes later in a quiet moment, and touch state or DOM nodes that no longer exist. Cancel in the same cleanup path that removes listeners.

saying these in an interview costs you the question

  • Thinks requestIdleCallback is just setTimeout with a longer delay
  • Writes deadline.timeRemaining > 0 without calling the method
  • Believes the browser interrupts a callback when the deadline hits
  • Assumes the deadline can be hundreds of milliseconds in a quiet tab
  • Never cancels the handle on component teardown

context