[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.
Andrew Clark committed
Nov 14, 2018 at 12:02 UTC
5bce0ef10ab28c87e57dd96dbe1d8470f8d68569
2 files changed
+27
-23
packages/scheduler/src/Scheduler.js
+17
-6
@@ -533,13 +533,14 @@ if (typeof window !== 'undefined' && window._schedMock) {
533
};
534
535
// We use the postMessage trick to defer idle work until after the repaint.
536
+ var port = null;
537
var messageKey =
538
'__reactIdleCallback$' +
539
Math.random()
540
.toString(36)
541
.slice(2);
542
var idleTick = function(event) {
542
- if (event.source !== window || event.data !== messageKey) {
543
+ if (event.source !== port || event.data !== messageKey) {
544
return;
545
}
546
@@ -583,9 +584,6 @@ if (typeof window !== 'undefined' && window._schedMock) {
584
}
585
}
586
};
586
- // Assumes that we have addEventListener in this environment. Might need
587
- // something better for old IE.
588
- window.addEventListener('message', idleTick, false);
587
588
var animationTick = function(rafTime) {
589
if (scheduledHostCallback !== null) {
@@ -629,7 +627,7 @@ if (typeof window !== 'undefined' && window._schedMock) {
627
frameDeadline = rafTime + activeFrameTime;
628
if (!isMessageEventScheduled) {
629
isMessageEventScheduled = true;
632
- window.postMessage(messageKey, '*');
630
+ port.postMessage(messageKey, '*');
631
}
632
};
633
@@ -638,7 +636,7 @@ if (typeof window !== 'undefined' && window._schedMock) {
636
timeoutTime = absoluteTimeout;
637
if (isFlushingHostCallback || absoluteTimeout < 0) {
638
// Don't wait for the next frame. Continue working ASAP, in a new event.
641
- window.postMessage(messageKey, '*');
639
+ port.postMessage(messageKey, '*');
640
} else if (!isAnimationFrameScheduled) {
641
// If rAF didn't already schedule one, we need to schedule a frame.
642
// TODO: If this rAF doesn't materialize because the browser throttles, we
@@ -649,6 +647,19 @@ if (typeof window !== 'undefined' && window._schedMock) {
647
}
648
};
649
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
+
663
cancelHostCallback = function() {
664
scheduledHostCallback = null;
665
isMessageEventScheduled = false;
packages/scheduler/src/__tests__/SchedulerDOM-test.js
+10
-17
@@ -64,33 +64,26 @@ describe('SchedulerDOM', () => {
64
let currentTime = 0;
65
66
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()
67
delete global.performance;
68
global.requestAnimationFrame = function(cb) {
69
return rAFCallbacks.push(() => {
70
cb(startOfLatestFrame);
71
});
72
};
79
- const originalAddEventListener = global.addEventListener;
80
- postMessageCallback = null;
73
postMessageEvents = [];
74
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
+ },
81
};
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;
85
};
86
+ postMessageCallback = event => port1.onmessage(event);
87
global.Date.now = function() {
88
return currentTime;
89
};