Dev.to AI 🤖 Ai 👁 0 📖 7 min read

requestIdleCallback vs setTimeout: The Delay I Actually Measured

On an idle page, requestIdleCallback and setTimeout(0) both fired in under 6ms across every run Under a simulated 8ms-per-frame render load, requestIdleCallback delay jumped to 13.6-28.8ms setTimeout(0) stayed under 0.3m

  • On an idle page, requestIdleCallback and setTimeout(0) both fired in under 6ms across every run

  • Under a simulated 8ms-per-frame render load, requestIdleCallback delay jumped to 13.6-28.8ms

  • setTimeout(0) stayed under 0.3ms delay in the same busy-page test, every single run

  • requestIdleCallback is the one that waits, not setTimeout, which is backwards from what most advice assumes

I'd always assumed requestIdleCallback was the polite one and setTimeout(0) was the one that could jump the queue if the page got busy. I built a small Playwright test in headless Chromium to check, and the result was the opposite of what I expected, in a way that's consistent enough to change how I schedule background work.

I was wrong about which one waits.

The setup is simple: schedule 50 callbacks of each kind, with the page idle, then again with a fake render loop burning roughly 8ms per animation frame, and measure the actual delay from "I scheduled this" to "this ran."

What I Actually Ran

The test page runs two scheduling functions, each wrapped so it records performance.now() at the moment of scheduling and again when the callback fires. Fifty callbacks per condition, four conditions total: idle page with requestIdleCallback, idle page with setTimeout(0), busy page with requestIdleCallback, busy page with setTimeout(0).

"Busy" isn't a vague simulation. A requestAnimationFrame loop burns roughly 8ms of synchronous main-thread work on every frame, which mimics a page doing real rendering or layout work, then schedules the next frame and repeats. I ran this in headless Chromium through Playwright, reading the actual pre-installed browser binary rather than guessing at numbers from memory.

On the idle page, both scheduling methods landed in a similar, small range across three separate runs: requestIdleCallback averaged between 1.7ms and 5.3ms, setTimeout(0) between 0.1ms and 1.9ms. Neither is slow enough to matter for almost anything you'd schedule this way. That part matched what I expected going in.

The busy-page numbers didn't. requestIdleCallback's average delay under the simulated render load came out to 28.85ms, 27.87ms, and 13.61ms across three separate runs. setTimeout(0) under the exact same load averaged 0.21ms, 0.2ms, and 0.13ms. Not close. Not a rounding difference. Two orders of magnitude apart, every time I ran it.

I reran the whole thing twice more to make sure the first result wasn't a fluke from a loaded test machine. Same shape both times: idle callback delay somewhere in the 13-29ms range under load, timeout delay never breaking 0.3ms.

The exact numbers moved between runs. Which method was ten to a hundred times slower never did.

Why requestIdleCallback Loses Here

The name is the trap. requestIdleCallback sounds like it should run whenever there's spare capacity, which under a busy page should mean "soon, in the gaps." In practice, the browser's definition of an idle period is conservative, and a page doing steady frame-driven work rarely produces a window long enough or early enough for the browser to confidently hand it over.

setTimeout(0), by contrast, isn't trying to detect idleness at all. It's just a macrotask with a near-zero delay, and the browser's event loop drains pending macrotasks after each task finishes, which includes right after a frame's rendering work completes. My fake render loop does its 8ms of work, returns, and the event loop immediately looks at what's waiting. The setTimeout callback is sitting right there, so it runs, often before the next animation frame is even due.

requestIdleCallback is specifically looking for a longer, cleaner gap, the kind that shows up when the page genuinely has nothing scheduled, not the kind that shows up for a few milliseconds between two rAF callbacks. Under my test's steady 8ms-per-frame load, that gap kept not qualifying, so the callback kept getting pushed. This lines up with a detail that's easy to skip past in the spec, the idle period the browser offers is bounded and can be skipped entirely if the browser decides there isn't one, and a timeout option only forces the callback to run late, not early.

I'd read this the same way I read the content-visibility numbers I measured on a 3,000-card grid: a browser API's name describes its intent, not its guaranteed behavior under load, and the only way to know which one wins on a specific page is to put a stopwatch on it.

Does the timeout Option Fix the Delay?

requestIdleCallback takes a second argument, { timeout: ms }, that's supposed to force the callback to run even if no idle period shows up in time. I tested it under the same busy-page load: once with no timeout at all, once with a deliberately short 10ms timeout.

It didn't help as cleanly as I expected. With no timeout, the callback ran after 19.18ms in one run and 13.55ms in another. With the 10ms timeout set, it ran after 11.54ms in one run and 15.47ms in the next. Those ranges overlap. The timeout option didn't reliably force an earlier run in my test, because the natural idle delay under this particular load was already close to the deadline I'd set.

That's worth understanding correctly rather than generalizing too far from two runs. The timeout option guarantees the idle callback fires by a certain point even without a true idle period. It doesn't guarantee it fires any sooner than the browser would have found a window anyway, and under a load pattern where idle windows are scarce, the deadline and the natural wait can end up close together. If you're relying on the timeout to bound worst-case latency, test that specific number against your own page's load instead of assuming it buys you much headroom. Treating the timeout option as a safety net for the deadline, not a performance fix, matches what I saw.

What This Changes About How I Schedule Background Work

If I'm deferring something that's genuinely low priority and the page is mostly idle, like persisting a draft to localStorage or pre-warming a cache, requestIdleCallback still does exactly what it says, and the small idle-page delay I measured (under 6ms either way) doesn't matter.

That's the case most tutorials use to recommend requestIdleCallback in the first place, a quiet page with nothing much happening, which is exactly the condition where the two methods are close enough that the choice barely matters. It's also the condition least likely to show you the gap, which is probably why the "requestIdleCallback is the polite, safe default" advice spread without much pushback.

The case I'd change is anything scheduled on a page I know is actively rendering, animating, or scrolling. That's exactly the condition where requestIdleCallback's delay stretched into the tens of milliseconds in my test, and a UI update sitting unexecuted for 20-30ms during active scrolling is the kind of jank a user actually notices. setTimeout(0) got the same work done without the wait, specifically because it doesn't bother waiting for a cleaner moment that a busy page may never offer.

I wouldn't go as far as "never use requestIdleCallback." The timeout option exists for a reason, and for work that's truly optional and fine running whenever, the API is doing its job correctly by deferring it. What I'd stop doing is reaching for requestIdleCallback as the default "defer this politely" choice on anything that touches visible UI during an active interaction. That instinct, which I've had for years, turned out to be backwards for exactly the page state where it mattered most.

The other adjustment is testing the actual condition you care about, not the idle one. It's easy to benchmark a scheduling API on a quiet tab, see a small number, and ship it. My idle-page numbers for both methods were close enough that either choice would've looked fine. The busy-page test is the one that actually separates them, and it's the state most real pages are in most of the time a user is doing something.

A page that's mid-scroll, mid-animation, or running any kind of continuous render loop is the busy condition, not an edge case. If the background work you're scheduling only matters when the user is doing something else on the page, that's precisely the moment my test showed requestIdleCallback struggling to find room.

There's a middle path I'd actually use in production: schedule with requestIdleCallback when it's available and the work is genuinely optional, but set a timeout short enough that the fallback deadline, not the natural idle search, is doing the real work of bounding latency. Given what I measured, I'd treat that timeout as a correctness guarantee against indefinite postponement, not a speed guarantee, and I'd pick its value from a real busy-page test on my own site, not a number borrowed from a blog post.

Bottom Line

requestIdleCallback isn't the safer, more polite default I assumed it was. On a page doing real frame work, it waited 13.6-28.8ms in my test, while setTimeout(0) stayed under 0.3ms every single run, because setTimeout doesn't wait for a clean idle window that a busy page may not produce often enough.

If you're deferring work on a page that's likely to be actively rendering, I'd default to setTimeout(0) and only reach for requestIdleCallback when you've confirmed the page is actually quiet most of the time it runs. Don't take my numbers as universal constants either. Browsers tune their idle-period heuristics, and a different render pattern than my 8ms-per-frame test could land differently.

Run the same four-condition test against your own page's actual load before you pick one, because the gap between "idle" and "busy" turned out to be the only number that mattered here. The test itself is maybe forty lines of code and a headless browser; there's no reason to guess at something this cheap to measure directly.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.