Skip to content

Commit 0ea3563

Browse files
acdliten8schloss
authored andcommitted
[scheduler] Post to MessageChannel instead of window (facebook#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.
1 parent d8a08fb commit 0ea3563

File tree

2 files changed

+27
-23
lines changed

2 files changed

+27
-23
lines changed

packages/scheduler/src/Scheduler.js

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -533,13 +533,14 @@ if (typeof window !== 'undefined' && window._schedMock) {
533533
};
534534

535535
// We use the postMessage trick to defer idle work until after the repaint.
536+
var port = null;
536537
var messageKey =
537538
'__reactIdleCallback$' +
538539
Math.random()
539540
.toString(36)
540541
.slice(2);
541542
var idleTick = function(event) {
542-
if (event.source !== window || event.data !== messageKey) {
543+
if (event.source !== port || event.data !== messageKey) {
543544
return;
544545
}
545546

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

590588
var animationTick = function(rafTime) {
591589
if (scheduledHostCallback !== null) {
@@ -629,7 +627,7 @@ if (typeof window !== 'undefined' && window._schedMock) {
629627
frameDeadline = rafTime + activeFrameTime;
630628
if (!isMessageEventScheduled) {
631629
isMessageEventScheduled = true;
632-
window.postMessage(messageKey, '*');
630+
port.postMessage(messageKey, '*');
633631
}
634632
};
635633

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

650+
if (typeof MessageChannel === 'function') {
651+
// Use a MessageChannel, if support exists
652+
var channel = new MessageChannel();
653+
channel.port1.onmessage = idleTick;
654+
port = channel.port2;
655+
} else {
656+
// Otherwise post a message to the window. This isn't ideal because message
657+
// handlers will fire on every frame until the queue is empty, including
658+
// some browser extensions.
659+
window.addEventListener('message', idleTick, false);
660+
port = window;
661+
}
662+
652663
cancelHostCallback = function() {
653664
scheduledHostCallback = null;
654665
isMessageEventScheduled = false;

packages/scheduler/src/__tests__/SchedulerDOM-test.js

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -64,33 +64,26 @@ describe('SchedulerDOM', () => {
6464
let currentTime = 0;
6565

6666
beforeEach(() => {
67-
// TODO pull this into helper method, reduce repetition.
68-
// mock the browser APIs which are used in schedule:
69-
// - requestAnimationFrame should pass the DOMHighResTimeStamp argument
70-
// - calling 'window.postMessage' should actually fire postmessage handlers
71-
// - Date.now should return the correct thing
72-
// - test with native performance.now()
7367
delete global.performance;
7468
global.requestAnimationFrame = function(cb) {
7569
return rAFCallbacks.push(() => {
7670
cb(startOfLatestFrame);
7771
});
7872
};
79-
const originalAddEventListener = global.addEventListener;
80-
postMessageCallback = null;
8173
postMessageEvents = [];
8274
postMessageErrors = [];
83-
global.addEventListener = function(eventName, callback, useCapture) {
84-
if (eventName === 'message') {
85-
postMessageCallback = callback;
86-
} else {
87-
originalAddEventListener(eventName, callback, useCapture);
88-
}
75+
const port1 = {};
76+
const port2 = {
77+
postMessage(messageKey) {
78+
const postMessageEvent = {source: port2, data: messageKey};
79+
postMessageEvents.push(postMessageEvent);
80+
},
8981
};
90-
global.postMessage = function(messageKey, targetOrigin) {
91-
const postMessageEvent = {source: window, data: messageKey};
92-
postMessageEvents.push(postMessageEvent);
82+
global.MessageChannel = function MessageChannel() {
83+
this.port1 = port1;
84+
this.port2 = port2;
9385
};
86+
postMessageCallback = event => port1.onmessage(event);
9487
global.Date.now = function() {
9588
return currentTime;
9689
};

0 commit comments

Comments
 (0)