From 5bce0ef10ab28c87e57dd96dbe1d8470f8d68569 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 14 Nov 2018 12:02:00 -0800 Subject: [PATCH] [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. --- packages/scheduler/src/Scheduler.js | 23 +++++++++++----- .../src/__tests__/SchedulerDOM-test.js | 27 +++++++------------ 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/packages/scheduler/src/Scheduler.js b/packages/scheduler/src/Scheduler.js index d2aee1b639db1..cc77e3a2c713d 100644 --- a/packages/scheduler/src/Scheduler.js +++ b/packages/scheduler/src/Scheduler.js @@ -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; diff --git a/packages/scheduler/src/__tests__/SchedulerDOM-test.js b/packages/scheduler/src/__tests__/SchedulerDOM-test.js index 9da18f7f5f66b..90ae7c5b1aadd 100644 --- a/packages/scheduler/src/__tests__/SchedulerDOM-test.js +++ b/packages/scheduler/src/__tests__/SchedulerDOM-test.js @@ -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; };