Skip to content

Commit

Permalink
[scheduler] Post to MessageChannel instead of window (facebook#14234)
Browse files Browse the repository at this point in the history
Scheduler needs to schedule a task that fires after paint. To do this,
it currently posts a message event to `window`. This happens on every
frame until the queue is empty. An unfortunate consequence is that every
other message event handler also gets called on every frame; even if
they exit immediately, this adds up to significant per-frame overhead.

Instead, we'll create a MessageChannel and post to that, with a
fallback to the old behavior if MessageChannel does not exist.
  • Loading branch information
acdlite authored and jetoneza committed Jan 23, 2019
1 parent 24fd835 commit 0e5d0df
Show file tree
Hide file tree
Showing 2 changed files with 27 additions and 23 deletions.
23 changes: 17 additions & 6 deletions packages/scheduler/src/Scheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -533,13 +533,14 @@ if (typeof window !== 'undefined' && window._schedMock) {
};

// We use the postMessage trick to defer idle work until after the repaint.
var port = null;
var messageKey =
'__reactIdleCallback$' +
Math.random()
.toString(36)
.slice(2);
var idleTick = function(event) {
if (event.source !== window || event.data !== messageKey) {
if (event.source !== port || event.data !== messageKey) {
return;
}

Expand Down Expand Up @@ -583,9 +584,6 @@ if (typeof window !== 'undefined' && window._schedMock) {
}
}
};
// Assumes that we have addEventListener in this environment. Might need
// something better for old IE.
window.addEventListener('message', idleTick, false);

var animationTick = function(rafTime) {
if (scheduledHostCallback !== null) {
Expand Down Expand Up @@ -629,7 +627,7 @@ if (typeof window !== 'undefined' && window._schedMock) {
frameDeadline = rafTime + activeFrameTime;
if (!isMessageEventScheduled) {
isMessageEventScheduled = true;
window.postMessage(messageKey, '*');
port.postMessage(messageKey, '*');
}
};

Expand All @@ -638,7 +636,7 @@ if (typeof window !== 'undefined' && window._schedMock) {
timeoutTime = absoluteTimeout;
if (isFlushingHostCallback || absoluteTimeout < 0) {
// Don't wait for the next frame. Continue working ASAP, in a new event.
window.postMessage(messageKey, '*');
port.postMessage(messageKey, '*');
} else if (!isAnimationFrameScheduled) {
// If rAF didn't already schedule one, we need to schedule a frame.
// TODO: If this rAF doesn't materialize because the browser throttles, we
Expand All @@ -649,6 +647,19 @@ if (typeof window !== 'undefined' && window._schedMock) {
}
};

if (typeof MessageChannel === 'function') {
// Use a MessageChannel, if support exists
var channel = new MessageChannel();
channel.port1.onmessage = idleTick;
port = channel.port2;
} else {
// Otherwise post a message to the window. This isn't ideal because message
// handlers will fire on every frame until the queue is empty, including
// some browser extensions.
window.addEventListener('message', idleTick, false);
port = window;
}

cancelHostCallback = function() {
scheduledHostCallback = null;
isMessageEventScheduled = false;
Expand Down
27 changes: 10 additions & 17 deletions packages/scheduler/src/__tests__/SchedulerDOM-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,33 +64,26 @@ describe('SchedulerDOM', () => {
let currentTime = 0;

beforeEach(() => {
// TODO pull this into helper method, reduce repetition.
// mock the browser APIs which are used in schedule:
// - requestAnimationFrame should pass the DOMHighResTimeStamp argument
// - calling 'window.postMessage' should actually fire postmessage handlers
// - Date.now should return the correct thing
// - test with native performance.now()
delete global.performance;
global.requestAnimationFrame = function(cb) {
return rAFCallbacks.push(() => {
cb(startOfLatestFrame);
});
};
const originalAddEventListener = global.addEventListener;
postMessageCallback = null;
postMessageEvents = [];
postMessageErrors = [];
global.addEventListener = function(eventName, callback, useCapture) {
if (eventName === 'message') {
postMessageCallback = callback;
} else {
originalAddEventListener(eventName, callback, useCapture);
}
const port1 = {};
const port2 = {
postMessage(messageKey) {
const postMessageEvent = {source: port2, data: messageKey};
postMessageEvents.push(postMessageEvent);
},
};
global.postMessage = function(messageKey, targetOrigin) {
const postMessageEvent = {source: window, data: messageKey};
postMessageEvents.push(postMessageEvent);
global.MessageChannel = function MessageChannel() {
this.port1 = port1;
this.port2 = port2;
};
postMessageCallback = event => port1.onmessage(event);
global.Date.now = function() {
return currentTime;
};
Expand Down

0 comments on commit 0e5d0df

Please sign in to comment.