[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:
parent
d7fd679a31
commit
5bce0ef10a
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue