Frontend Development 14 min read

React Scheduler: Implementing Idle‑Time Execution and Time Slicing with requestAnimationFrame, postMessage and MessageChannel

This article explains how React simulates requestIdleCallback by combining requestAnimationFrame, postMessage, and MessageChannel to execute work during browser idle periods, detailing the evolution from the early rAF‑postMessage approach in v16 to the message‑loop implementation in v18 and providing simplified source code examples.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
React Scheduler: Implementing Idle‑Time Execution and Time Slicing with requestAnimationFrame, postMessage and MessageChannel

In the previous article we introduced requestIdleCallback and its limitations, noting that React does not use it directly. This piece explores how React achieves similar idle‑time execution through its own scheduler, using time slicing to avoid blocking critical events such as animation and input.

setTimeout Polyfill

MDN offers a fallback implementation using setTimeout , but it does not provide true idle‑time behavior because it cannot guarantee execution only when the browser is idle.

window.requestIdleCallback = window.requestIdleCallback || function(handler) {
  let startTime = Date.now();
  return setTimeout(function() {
    handler({
      didTimeout: false,
      timeRemaining: function() {
        return Math.max(0, 50.0 - (Date.now() - startTime));
      }
    });
  }, 1);
};

postMessage Introduction

React’s early implementation used requestAnimationFrame together with window.postMessage to schedule work. postMessage enables safe cross‑origin communication and can be used as a zero‑delay macro‑task.

window.addEventListener("message", (e) => console.log(e.data), false);
window.postMessage('Hello World!');

React v16 Implementation (rAF + postMessage)

The simplified v16 scheduler polyfills requestIdleCallback (named rIC ) by using requestAnimationFrame to estimate the frame deadline and then posting a message to trigger the idle callback.

// Polyfill requestIdleCallback.
var scheduledRICCallback = null;
var frameDeadline = 0;
var activeFrameTime = 33; // assume 30fps => 33ms per frame
var frameDeadlineObject = {
  timeRemaining: function() {
    return frameDeadline - performance.now();
  }
};
var idleTick = function(event) {
  scheduledRICCallback(frameDeadlineObject);
};
window.addEventListener('message', idleTick, false);
var animationTick = function(rafTime) {
  frameDeadline = rafTime + activeFrameTime;
  window.postMessage('__reactIdleCallback$1', '*');
};
var rIC = function(callback) {
  scheduledRICCallback = callback;
  requestAnimationFrame(animationTick);
  return 0;
};

The logic schedules rIC to run after the current frame, calculates the remaining time, and invokes the callback without blocking higher‑priority tasks.

Removal of rAF in React v16.2.0

Later versions replaced the rAF‑based approach with a pure message‑loop using MessageChannel , because rAF can be paused in background tabs, affecting performance.

MessageChannel Introduction

MessageChannel creates two linked MessagePort objects for communication, similar to postMessage but without cross‑origin restrictions.

var channel = new MessageChannel();
channel.port1.onmessage = (e) => console.log(e.data);
channel.port2.postMessage('Hello World');

React v18 Implementation (message loop)

The v18 scheduler renames rIC to requestHostCallback . It records the callback, starts a message loop, and uses a MessageChannel to repeatedly post a message, allowing the browser to interleave work with rendering and input handling.

let scheduledHostCallback;
let isMessageLoopRunning = false;
let getCurrentTime = () => performance.now();
let startTime;
function requestHostCallback(callback) {
  scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
let schedulePerformWorkUntilDeadline = () => {
  port.postMessage(null);
};
function performWorkUntilDeadline() {
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    startTime = currentTime;
    const hasTimeRemaining = true;
    let hasMoreWork = true;
    try {
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {
      if (hasMoreWork) {
        schedulePerformWorkUntilDeadline();
      } else {
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      }
    }
  } else {
    isMessageLoopRunning = false;
  }
}
function flushWork(hasTimeRemaining, initialTime) {
  return workLoop(hasTimeRemaining, initialTime);
}
let currentTask;
function workLoop(hasTimeRemaining, initialTime) {
  currentTask = taskQueue[0];
  while (currentTask != null) {
    if (currentTask.expirationTime > initialTime && (!hasTimeRemaining || shouldYieldToHost())) {
      break;
    }
    const callback = currentTask.callback;
    callback();
    taskQueue.shift();
    currentTask = taskQueue[0];
  }
  return currentTask !== null;
}
const frameInterval = 5;
function shouldYieldToHost() {
  const timeElapsed = getCurrentTime() - startTime;
  return timeElapsed >= frameInterval;
}
requestHostCallback(flushWork);

The scheduler repeatedly executes tasks from taskQueue for up to the frame interval (default 5 ms). When the time slice expires, it posts a message to yield the thread, letting the browser handle higher‑priority work before resuming.

Time Slicing Summary

React breaks updates into small tasks, runs them in a loop, and after each slice checks whether the allotted time has been exceeded. If so, it yields via postMessage , allowing the browser to process animation and input, then continues processing remaining tasks on the next message.

Complete Simplified Example

// Simulate work duration
const sleep = delay => {
  for (let start = Date.now(); Date.now() - start <= delay;) {}
};
let startTime;
let scheduledHostCallback;
let isMessageLoopRunning = false;
let getCurrentTime = () => performance.now();
const taskQueue = [{
  expirationTime: 1000000,
  callback: () => { sleep(30); console.log(1); }
}, {
  expirationTime: 1000000,
  callback: () => { sleep(30); console.log(2); }
}, {
  expirationTime: 1000000,
  callback: () => { sleep(30); console.log(3); }
}];
function requestHostCallback(callback) {
  scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}
const channel = new MessageChannel();
const port = channel.port2;
function performWorkUntilDeadline() {
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    startTime = currentTime;
    const hasTimeRemaining = true;
    let hasMoreWork = true;
    try {
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {
      console.log('hasMoreWork', hasMoreWork);
      if (hasMoreWork) {
        schedulePerformWorkUntilDeadline();
      } else {
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      }
    }
  } else {
    isMessageLoopRunning = false;
  }
}
channel.port1.onmessage = performWorkUntilDeadline;
let schedulePerformWorkUntilDeadline = () => {
  port.postMessage(null);
};
function flushWork(hasTimeRemaining, initialTime) {
  return workLoop(hasTimeRemaining, initialTime);
}
let currentTask;
function workLoop(hasTimeRemaining, initialTime) {
  currentTask = taskQueue[0];
  while (currentTask != null) {
    console.log(currentTask);
    if (currentTask.expirationTime > initialTime && (!hasTimeRemaining || shouldYieldToHost())) {
      break;
    }
    const callback = currentTask.callback;
    callback();
    taskQueue.shift();
    currentTask = taskQueue[0];
  }
  return currentTask != null;
}
const frameInterval = 5;
function shouldYieldToHost() {
  const timeElapsed = getCurrentTime() - startTime;
  return timeElapsed >= frameInterval;
}
requestHostCallback(flushWork);

By combining requestAnimationFrame , postMessage , and MessageChannel , React’s scheduler achieves cooperative multitasking, ensuring smooth UI updates without compromising responsiveness.

FrontendreactschedulerMessageChannelTime SlicingrequestIdleCallback
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.