[scheduler] Post to MessageChannel instead of window (#14234)

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.
This commit is contained in:
Andrew Clark 2018-11-14 12:02:00 -08:00 committed by GitHub
parent d7fd679a31
commit 5bce0ef10a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 27 additions and 23 deletions

View File

@ -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;
}
@ -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) {
@ -629,7 +627,7 @@ if (typeof window !== 'undefined' && window._schedMock) {
frameDeadline = rafTime + activeFrameTime;
if (!isMessageEventScheduled) {
isMessageEventScheduled = true;
window.postMessage(messageKey, '*');
port.postMessage(messageKey, '*');
}
};
@ -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
@ -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;

View File

@ -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;
};