Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

Do the Boring Work When the Browser Is Bored: requestIdleCallback

C
Carlo Fontanos
· 2 min read

Main-thread time is the scarcest resource in frontend work. Every millisecond you spend preparing analytics or warming caches is a millisecond a tap or keypress waits. The browser knows when it's idle - requestIdleCallback lets you borrow exactly those moments.

requestIdleCallback((deadline) => {
    while (deadline.timeRemaining() > 0 && tasks.length) {
        tasks.shift()();   // run small tasks until the idle window closes
    }
    if (tasks.length) scheduleMore();
});

The deadline object is the clever part: timeRemaining() tells you how many milliseconds of idle time are left (capped at 50ms to keep the page responsive to unexpected input). You do work while there's budget, then yield.

A reusable idle queue

The pattern above generalizes into ten lines I've carried between projects:

const idleQueue = {
    tasks: [],
    push(fn) {
        this.tasks.push(fn);
        this.schedule();
    },
    schedule() {
        if (this.scheduled || !this.tasks.length) return;
        this.scheduled = true;
        requestIdleCallback((d) => {
            this.scheduled = false;
            while (d.timeRemaining() > 1 && this.tasks.length) this.tasks.shift()();
            this.schedule();   // more tasks left? grab the next idle window
        });
    },
};

// Anywhere in the app:
idleQueue.push(() => prefetchRoute('/checkout'));
idleQueue.push(() => warmImageCache(nextProductImages));
idleQueue.push(() => compressAndStoreDraft(editorState));

Keep each task small (a few ms). If one task might be long, chunk it - the whole point is fitting into gaps.

The timeout escape hatch

Pure idle scheduling means "maybe never" on a busy page. If the work must eventually happen, say so:

requestIdleCallback(sendQueuedAnalytics, { timeout: 2000 });

If no idle window shows up within 2 seconds, the callback runs anyway (with deadline.didTimeout === true). Idle-first, deadline-guaranteed.

The Safari problem

Safari took ages to ship this API - it only arrived recently, so check support and fall back:

const onIdle = window.requestIdleCallback
    ?? ((fn) => setTimeout(() => fn({ timeRemaining: () => 8, didTimeout: false }), 200));

The setTimeout fallback isn't truly idle-aware, but a 200ms delay past load keeps you out of the critical path in practice.

Rule of thumb I use: if the user didn't ask for it and won't notice a two-second delay, it belongs in the idle queue. For work triggered by scrolling into view instead, IntersectionObserver is the right scheduler.

C
Written by Carlo Fontanos

Full-stack web developer sharing practical tutorials and building tools that ship.

Got something on your mind?

My inbox is open - no forms disappearing into the void here.

  • Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
  • Hire me for a project Have something custom in mind? Let's talk scope and timelines.
  • Product support Bought something here? I'll help you get it running.

I usually reply within 1-2 business days.

Message sent!

Your details are only used to reply to you.

Keep reading