@samitouri / QOS-React / commits / 5bce0ef10a

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