From 11f6b721929fa888025346cf6f8f31a0dc00eb0c Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Thu, 15 Feb 2018 13:52:56 -0800 Subject: [PATCH 01/14] Create request-idle-callback-polyfill package Sets up the basic structure for the new package along with some empty tests. Pulls out the relevant code from ReactDOMFrameScheduling as a starting point for the polyfill. --- .../request-idle-callback-polyfill/README.md | 4 + .../request-idle-callback-polyfill/index.js | 12 ++ .../npm/index.js | 10 + .../package.json | 22 +++ .../src/RequestIdleCallback.js | 175 ++++++++++++++++++ .../src/__tests__/RequestIdleCallback-test.js | 41 ++++ scripts/rollup/bundles.js | 9 + 7 files changed, 273 insertions(+) create mode 100644 packages/request-idle-callback-polyfill/README.md create mode 100644 packages/request-idle-callback-polyfill/index.js create mode 100644 packages/request-idle-callback-polyfill/npm/index.js create mode 100644 packages/request-idle-callback-polyfill/package.json create mode 100644 packages/request-idle-callback-polyfill/src/RequestIdleCallback.js create mode 100644 packages/request-idle-callback-polyfill/src/__tests__/RequestIdleCallback-test.js diff --git a/packages/request-idle-callback-polyfill/README.md b/packages/request-idle-callback-polyfill/README.md new file mode 100644 index 0000000000000..d09bfd445a75b --- /dev/null +++ b/packages/request-idle-callback-polyfill/README.md @@ -0,0 +1,4 @@ +# request-idle-callback-polyfill + +A polyfill for `requestIdleCallback` + diff --git a/packages/request-idle-callback-polyfill/index.js b/packages/request-idle-callback-polyfill/index.js new file mode 100644 index 0000000000000..b26ec6a1b8c1b --- /dev/null +++ b/packages/request-idle-callback-polyfill/index.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +'use strict'; + +export * from './src/RequestIdleCallback'; diff --git a/packages/request-idle-callback-polyfill/npm/index.js b/packages/request-idle-callback-polyfill/npm/index.js new file mode 100644 index 0000000000000..065e3b02964d3 --- /dev/null +++ b/packages/request-idle-callback-polyfill/npm/index.js @@ -0,0 +1,10 @@ +'use strict'; + +// @TODO figure out if we need a prod/dev build for this? +// if (process.env.NODE_ENV === 'production') { +// module.exports = require('./cjs/{FILE}'); +// } else { +// module.exports = require('./cjs/{FILE}'); +// } + +module.exports = require('./cjs/request-idle-callback-polyfill.js'); \ No newline at end of file diff --git a/packages/request-idle-callback-polyfill/package.json b/packages/request-idle-callback-polyfill/package.json new file mode 100644 index 0000000000000..f5823900c99de --- /dev/null +++ b/packages/request-idle-callback-polyfill/package.json @@ -0,0 +1,22 @@ +{ + "name": "request-idle-callback-polyfill", + "description": "A polyfill for requestIdleCallback, used by React.", + "version": "0.1.0-alpha.1", + "keywords": [ + "requestIdleCallback" + ], + "homepage": "https://facebook.github.io/react/", + "bugs": "https://github.com/facebook/react/issues", + "license": "MIT", + "files": [ + "LICENSE", + "README.md", + "index.js" + ], + "main": "index.js", + "repository": "facebook/react", + "dependencies": { + "fbjs": "^0.8.16" + } + } + \ No newline at end of file diff --git a/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js b/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js new file mode 100644 index 0000000000000..4b04815402300 --- /dev/null +++ b/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js @@ -0,0 +1,175 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +// This is a built-in polyfill for requestIdleCallback. It works by scheduling +// a requestAnimationFrame, storing the time for the start of the frame, then +// scheduling a postMessage which gets scheduled after paint. Within the +// postMessage handler do as much work as possible until time + frame rate. +// By separating the idle call into a separate event tick we ensure that +// layout, paint and other browser work is counted against the available time. +// The frame rate is dynamically adjusted. + +export type IdleDeadline = { + timeRemaining: () => number, + didTimeout: boolean, +}; + +const hasNativePerformanceNow = + typeof performance === 'object' && typeof performance.now === 'function'; + +let now; +if (hasNativePerformanceNow) { + now = function() { + return performance.now(); + }; +} else { + now = function() { + return Date.now(); + }; +} + + let scheduledRICCallback = null; + let isIdleScheduled = false; + let timeoutTime = -1; + + let isAnimationFrameScheduled = false; + + let frameDeadline = 0; + // We start out assuming that we run at 30fps but then the heuristic tracking + // will adjust this value to a faster fps if we get more frequent animation + // frames. + let previousFrameTime = 33; + let activeFrameTime = 33; + + let frameDeadlineObject; + if (hasNativePerformanceNow) { + frameDeadlineObject = { + didTimeout: false, + timeRemaining() { + // We assume that if we have a performance timer that the rAF callback + // gets a performance timer value. Not sure if this is always true. + const remaining = frameDeadline - performance.now(); + return remaining > 0 ? remaining : 0; + }, + }; + } else { + frameDeadlineObject = { + didTimeout: false, + timeRemaining() { + // Fallback to Date.now() + const remaining = frameDeadline - Date.now(); + return remaining > 0 ? remaining : 0; + }, + }; + } + + // We use the postMessage trick to defer idle work until after the repaint. + const messageKey = + '__reactIdleCallback$' + + Math.random() + .toString(36) + .slice(2); + const idleTick = function(event) { + if (event.source !== window || event.data !== messageKey) { + return; + } + + isIdleScheduled = false; + + const currentTime = now(); + if (frameDeadline - currentTime <= 0) { + // There's no time left in this idle period. Check if the callback has + // a timeout and whether it's been exceeded. + if (timeoutTime !== -1 && timeoutTime <= currentTime) { + // Exceeded the timeout. Invoke the callback even though there's no + // time left. + frameDeadlineObject.didTimeout = true; + } else { + // No timeout. + if (!isAnimationFrameScheduled) { + // Schedule another animation callback so we retry later. + isAnimationFrameScheduled = true; + requestAnimationFrame(animationTick); + } + // Exit without invoking the callback. + return; + } + } else { + // There's still time left in this idle period. + frameDeadlineObject.didTimeout = false; + } + + timeoutTime = -1; + const callback = scheduledRICCallback; + scheduledRICCallback = null; + if (callback !== null) { + callback(frameDeadlineObject); + } + }; + // Assumes that we have addEventListener in this environment. Might need + // something better for old IE. + window.addEventListener('message', idleTick, false); + + const animationTick = function(rafTime) { + isAnimationFrameScheduled = false; + let nextFrameTime = rafTime - frameDeadline + activeFrameTime; + if ( + nextFrameTime < activeFrameTime && + previousFrameTime < activeFrameTime + ) { + if (nextFrameTime < 8) { + // Defensive coding. We don't support higher frame rates than 120hz. + // If we get lower than that, it is probably a bug. + nextFrameTime = 8; + } + // If one frame goes long, then the next one can be short to catch up. + // If two frames are short in a row, then that's an indication that we + // actually have a higher frame rate than what we're currently optimizing. + // We adjust our heuristic dynamically accordingly. For example, if we're + // running on 120hz display or 90hz VR display. + // Take the max of the two in case one of them was an anomaly due to + // missed frame deadlines. + activeFrameTime = + nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime; + } else { + previousFrameTime = nextFrameTime; + } + frameDeadline = rafTime + activeFrameTime; + if (!isIdleScheduled) { + isIdleScheduled = true; + window.postMessage(messageKey, '*'); + } + }; + + export function requestIdleCallback( + callback: (deadline: IdleDeadline) => void, + options?: {timeout: number}, + ): number { + // This assumes that we only schedule one callback at a time because that's + // how Fiber uses it. + scheduledRICCallback = callback; + if (options != null && typeof options.timeout === 'number') { + timeoutTime = now() + options.timeout; + } + 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 + // might want to still have setTimeout trigger rIC as a backup to ensure + // that we keep performing work. + isAnimationFrameScheduled = true; + requestAnimationFrame(animationTick); + } + return 0; + }; + + export function cancelIdleCallback() { + scheduledRICCallback = null; + isIdleScheduled = false; + timeoutTime = -1; + }; diff --git a/packages/request-idle-callback-polyfill/src/__tests__/RequestIdleCallback-test.js b/packages/request-idle-callback-polyfill/src/__tests__/RequestIdleCallback-test.js new file mode 100644 index 0000000000000..7f8daeed48bc7 --- /dev/null +++ b/packages/request-idle-callback-polyfill/src/__tests__/RequestIdleCallback-test.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @desc adopted from w3c/web-platform-tests + * @see https://github.com/w3c/web-platform-tests/tree/d977904a0af38c7e2d28c6e1327fb437c2b2e0da/requestidlecallback + * + * @emails react-core + */ + +'use strict'; + +let requestIdleCallback; + +describe('RequestIdleCallback', () => { + beforeEach(() => { + jest.resetModules(); + + requestIdleCallback = require('request-idle-callback-polyfill'); + }); + + describe('requestIdleCallback', () => { + it('should be a function', () => {}); + it('returns a number', () => {}); + it('exceptions are reported to error handlers', () => {}); + it('nested callbacks get a new idle period', () => {}); + it('nested callbacks dont get the same deadline', () => {}); + it('invoked at least once before the timeout', () => {}); + it('callbacks invoked in order (called iteratively)', () => {}); + it('callbacks invoked in order (called recursively)', () => {}); + + + }); + + descibe('cancelIdleCallback', () => { + it('should be a function', () => {}); + it('cancels a callback', () => {}); + }); +}); diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 0be2375a58b9a..33521f0d8cbef 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -254,6 +254,15 @@ const bundles = [ global: 'SimpleCacheProvider', externals: ['react'], }, + /******* Request Idle Callback Polfyill *******/ + { + label: 'request-idle-callback-polyfill', + bundleTypes: [NODE_DEV, NODE_PROD, UMD_DEV, UMD_PROD], + moduleType: ISOMORPHIC, + entry: 'request-idle-callback-polyfill', + global: 'requestIdleCallback', + externals: [], + }, ]; // Based on deep-freeze by substack (public domain) From 09ec46e38be9f7a86a6a5ee29773e990916748d9 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Thu, 15 Feb 2018 16:55:13 -0800 Subject: [PATCH 02/14] Add some spec-defined types --- .../src/RequestIdleCallback.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js b/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js index 4b04815402300..005c3ad27cc9e 100644 --- a/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js +++ b/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js @@ -15,11 +15,18 @@ // layout, paint and other browser work is counted against the available time. // The frame rate is dynamically adjusted. -export type IdleDeadline = { +export type IdleDeadline = { timeRemaining: () => number, didTimeout: boolean, }; +type IdleRequestOptions = { + timeout: number, +} + +type IdleRequestCallback = (IdleDeadline) => void; + + const hasNativePerformanceNow = typeof performance === 'object' && typeof performance.now === 'function'; @@ -148,8 +155,8 @@ if (hasNativePerformanceNow) { }; export function requestIdleCallback( - callback: (deadline: IdleDeadline) => void, - options?: {timeout: number}, + callback: IdleRequestCallback, + options?: IdleRequestOptions, ): number { // This assumes that we only schedule one callback at a time because that's // how Fiber uses it. From 882ab492fb96734dab262d706cd07c5f89026675 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Thu, 15 Feb 2018 16:57:35 -0800 Subject: [PATCH 03/14] Rename local variables to be consistent with spec This makes it a little easier understand when trying to implement spec-compliant behavior --- .../src/RequestIdleCallback.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js b/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js index 005c3ad27cc9e..2f0b03cef7a64 100644 --- a/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js +++ b/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js @@ -41,6 +41,8 @@ if (hasNativePerformanceNow) { }; } +let lastIdlePeriodDeadline = 0; + let scheduledRICCallback = null; let isIdleScheduled = false; let timeoutTime = -1; @@ -90,7 +92,7 @@ if (hasNativePerformanceNow) { isIdleScheduled = false; const currentTime = now(); - if (frameDeadline - currentTime <= 0) { + if (lastIdlePeriodDeadline - currentTime <= 0) { // There's no time left in this idle period. Check if the callback has // a timeout and whether it's been exceeded. if (timeoutTime !== -1 && timeoutTime <= currentTime) { @@ -123,9 +125,9 @@ if (hasNativePerformanceNow) { // something better for old IE. window.addEventListener('message', idleTick, false); - const animationTick = function(rafTime) { +function animationTick(rafTime: number) { isAnimationFrameScheduled = false; - let nextFrameTime = rafTime - frameDeadline + activeFrameTime; + let nextFrameTime = rafTime - lastIdlePeriodDeadline + activeFrameTime; if ( nextFrameTime < activeFrameTime && previousFrameTime < activeFrameTime @@ -147,7 +149,7 @@ if (hasNativePerformanceNow) { } else { previousFrameTime = nextFrameTime; } - frameDeadline = rafTime + activeFrameTime; + lastIdlePeriodDeadline = rafTime + activeFrameTime; if (!isIdleScheduled) { isIdleScheduled = true; window.postMessage(messageKey, '*'); From fe28f0ad7790586d0dfda4b75054b5bd1dfd3c4b Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Thu, 15 Feb 2018 16:59:07 -0800 Subject: [PATCH 04/14] Recreate frameDeadlineObj every time a callback is called This is a prerequisite of supporting multiple scheduled callbacks. --- .../src/RequestIdleCallback.js | 28 ++++++------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js b/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js index 2f0b03cef7a64..50a0e8c991542 100644 --- a/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js +++ b/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js @@ -56,26 +56,12 @@ let lastIdlePeriodDeadline = 0; let previousFrameTime = 33; let activeFrameTime = 33; - let frameDeadlineObject; - if (hasNativePerformanceNow) { - frameDeadlineObject = { - didTimeout: false, - timeRemaining() { + +function timeRemaining() { // We assume that if we have a performance timer that the rAF callback // gets a performance timer value. Not sure if this is always true. - const remaining = frameDeadline - performance.now(); - return remaining > 0 ? remaining : 0; - }, - }; - } else { - frameDeadlineObject = { - didTimeout: false, - timeRemaining() { - // Fallback to Date.now() - const remaining = frameDeadline - Date.now(); + const remaining = lastIdlePeriodDeadline - now(); return remaining > 0 ? remaining : 0; - }, - }; } // We use the postMessage trick to defer idle work until after the repaint. @@ -84,12 +70,14 @@ let lastIdlePeriodDeadline = 0; Math.random() .toString(36) .slice(2); + const idleTick = function(event) { if (event.source !== window || event.data !== messageKey) { return; } isIdleScheduled = false; + let didTimeout = false; const currentTime = now(); if (lastIdlePeriodDeadline - currentTime <= 0) { @@ -98,7 +86,7 @@ let lastIdlePeriodDeadline = 0; if (timeoutTime !== -1 && timeoutTime <= currentTime) { // Exceeded the timeout. Invoke the callback even though there's no // time left. - frameDeadlineObject.didTimeout = true; + didTimeout = true; } else { // No timeout. if (!isAnimationFrameScheduled) { @@ -111,14 +99,14 @@ let lastIdlePeriodDeadline = 0; } } else { // There's still time left in this idle period. - frameDeadlineObject.didTimeout = false; + didTimeout = false; } timeoutTime = -1; const callback = scheduledRICCallback; scheduledRICCallback = null; if (callback !== null) { - callback(frameDeadlineObject); + callback({didTimeout, timeRemaining}); } }; // Assumes that we have addEventListener in this environment. Might need From 4a5134f4be6b728de5537f88f624592819fb3343 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Fri, 16 Feb 2018 14:28:30 -0800 Subject: [PATCH 05/14] Add support for scheduling multiple callbacks --- .../src/RequestIdleCallback.js | 213 ++++++++++-------- 1 file changed, 113 insertions(+), 100 deletions(-) diff --git a/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js b/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js index 50a0e8c991542..28c076c4eee56 100644 --- a/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js +++ b/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js @@ -22,71 +22,89 @@ export type IdleDeadline = { type IdleRequestOptions = { timeout: number, -} - -type IdleRequestCallback = (IdleDeadline) => void; +}; +type IdleRequestCallback = IdleDeadline => void; const hasNativePerformanceNow = typeof performance === 'object' && typeof performance.now === 'function'; let now; if (hasNativePerformanceNow) { - now = function() { + now = function () { return performance.now(); }; } else { - now = function() { + now = function () { return Date.now(); }; } -let lastIdlePeriodDeadline = 0; - - let scheduledRICCallback = null; - let isIdleScheduled = false; - let timeoutTime = -1; +function IdleDeadlineImpl(deadline : number, didTimeout: boolean) { + this._deadline = deadline; + this.didTimeout = didTimeout; +}; - let isAnimationFrameScheduled = false; +IdleDeadlineImpl.prototype.timeRemaining = function() { + // If the callback timed out there's definitely no time remaining + if (this.didTimeout) { + return 0; + } + // We assume that if we have a performance timer that the rAF callback + // gets a performance timer value. Not sure if this is always true. + const remaining = this._deadline - now(); + return remaining > 0 ? remaining : 0; +} - let frameDeadline = 0; - // We start out assuming that we run at 30fps but then the heuristic tracking - // will adjust this value to a faster fps if we get more frequent animation - // frames. - let previousFrameTime = 33; - let activeFrameTime = 33; +const idleCallbacks: Array = []; +const idleCallbackTimeouts : Array = []; +let idleCallbackIdentifier = 0; +let currentIdleCallbackHandle = 0; +let lastIdlePeriodDeadline = 0; -function timeRemaining() { - // We assume that if we have a performance timer that the rAF callback - // gets a performance timer value. Not sure if this is always true. - const remaining = lastIdlePeriodDeadline - now(); - return remaining > 0 ? remaining : 0; +let scheduledRICCallback = null; +let isIdleScheduled = false; + +let isAnimationFrameScheduled = false; +// We start out assuming that we run at 30fps but then the heuristic tracking +// will adjust this value to a faster fps if we get more frequent animation +// frames. +let previousFrameTime = 33; +let activeFrameTime = 33; + +// We use the postMessage trick to defer idle work until after the repaint. +const messageKey = + '__reactIdleCallback$' + + Math.random() + .toString(36) + .slice(2); + +const idleTick = function (event) { + if (event.source !== window || event.data !== messageKey) { + return; } - // We use the postMessage trick to defer idle work until after the repaint. - const messageKey = - '__reactIdleCallback$' + - Math.random() - .toString(36) - .slice(2); - - const idleTick = function(event) { - if (event.source !== window || event.data !== messageKey) { - return; + isIdleScheduled = false; + // While there are still callbacks in the queue... + while (currentIdleCallbackHandle < idleCallbacks.length) { + // Get the callback and the timeout, if it exists + const timeoutTime = idleCallbackTimeouts[currentIdleCallbackHandle]; + const callback = idleCallbacks[currentIdleCallbackHandle]; + // This callback might have been cancelled, continue to check the rest of the queue + if (!callback) { + currentIdleCallbackHandle++; + continue; } - - isIdleScheduled = false; - let didTimeout = false; - const currentTime = now(); - if (lastIdlePeriodDeadline - currentTime <= 0) { + let didTimeout = false; + if (lastIdlePeriodDeadline - currentTime <= 0) { // There's no time left in this idle period. Check if the callback has // a timeout and whether it's been exceeded. - if (timeoutTime !== -1 && timeoutTime <= currentTime) { + if (timeoutTime != null && timeoutTime <= currentTime) { // Exceeded the timeout. Invoke the callback even though there's no // time left. - didTimeout = true; + didTimeout = true; } else { // No timeout. if (!isAnimationFrameScheduled) { @@ -99,74 +117,69 @@ function timeRemaining() { } } else { // There's still time left in this idle period. - didTimeout = false; + didTimeout = false; } - - timeoutTime = -1; - const callback = scheduledRICCallback; - scheduledRICCallback = null; - if (callback !== null) { - callback({didTimeout, timeRemaining}); - } - }; - // Assumes that we have addEventListener in this environment. Might need - // something better for old IE. - window.addEventListener('message', idleTick, false); + currentIdleCallbackHandle++; + callback(new IdleDeadlineImpl(lastIdlePeriodDeadline, didTimeout)); + } +}; +// Assumes that we have addEventListener in this environment. Might need +// something better for old IE. +window.addEventListener('message', idleTick, false); function animationTick(rafTime: number) { - isAnimationFrameScheduled = false; + isAnimationFrameScheduled = false; let nextFrameTime = rafTime - lastIdlePeriodDeadline + activeFrameTime; - if ( - nextFrameTime < activeFrameTime && - previousFrameTime < activeFrameTime - ) { - if (nextFrameTime < 8) { - // Defensive coding. We don't support higher frame rates than 120hz. - // If we get lower than that, it is probably a bug. - nextFrameTime = 8; - } - // If one frame goes long, then the next one can be short to catch up. - // If two frames are short in a row, then that's an indication that we - // actually have a higher frame rate than what we're currently optimizing. - // We adjust our heuristic dynamically accordingly. For example, if we're - // running on 120hz display or 90hz VR display. - // Take the max of the two in case one of them was an anomaly due to - // missed frame deadlines. - activeFrameTime = - nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime; - } else { - previousFrameTime = nextFrameTime; + if (nextFrameTime < activeFrameTime && previousFrameTime < activeFrameTime) { + if (nextFrameTime < 8) { + // Defensive coding. We don't support higher frame rates than 120hz. + // If we get lower than that, it is probably a bug. + nextFrameTime = 8; } + // If one frame goes long, then the next one can be short to catch up. + // If two frames are short in a row, then that's an indication that we + // actually have a higher frame rate than what we're currently optimizing. + // We adjust our heuristic dynamically accordingly. For example, if we're + // running on 120hz display or 90hz VR display. + // Take the max of the two in case one of them was an anomaly due to + // missed frame deadlines. + activeFrameTime = + nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime; + } else { + previousFrameTime = nextFrameTime; + } lastIdlePeriodDeadline = rafTime + activeFrameTime; - if (!isIdleScheduled) { - isIdleScheduled = true; - window.postMessage(messageKey, '*'); - } - }; + if (!isIdleScheduled) { + isIdleScheduled = true; + window.postMessage(messageKey, '*'); + } +} - export function requestIdleCallback( +export function requestIdleCallback( callback: IdleRequestCallback, options?: IdleRequestOptions, - ): number { - // This assumes that we only schedule one callback at a time because that's - // how Fiber uses it. - scheduledRICCallback = callback; - if (options != null && typeof options.timeout === 'number') { - timeoutTime = now() + options.timeout; - } - 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 - // might want to still have setTimeout trigger rIC as a backup to ensure - // that we keep performing work. - isAnimationFrameScheduled = true; - requestAnimationFrame(animationTick); - } - return 0; - }; +): number { + scheduledRICCallback = callback; + const handle = idleCallbackIdentifier++; + idleCallbacks[handle] = callback; - export function cancelIdleCallback() { - scheduledRICCallback = null; - isIdleScheduled = false; - timeoutTime = -1; - }; + if (options != null && typeof options.timeout === 'number') { + idleCallbackTimeouts[handle] = now() + options.timeout; + } + 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 + // might want to still have setTimeout trigger rIC as a backup to ensure + // that we keep performing work. + isAnimationFrameScheduled = true; + requestAnimationFrame(animationTick); + } + return 0; +} + +export function cancelIdleCallback(handle: number) { + idleCallbacks[handle] = null; + idleCallbackTimeouts[handle] = null; + // @TODO this isn't true if there are still scheduled callbacks in the queue + isIdleScheduled = false; +} From f1be2779c5bdf7fd6f14347c0f5d79f15896642a Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Tue, 20 Feb 2018 09:36:57 -0800 Subject: [PATCH 06/14] Use setTimeout to call timed out callbacks idleTick calculates whether a callback is timed out, but this still depends on the callback queue being processed. Use setTimeout as a fallback so that callbacks with timeouts definitely get called within a reasonable timeframe. --- .../src/RequestIdleCallback.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js b/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js index 28c076c4eee56..827f88ef207f2 100644 --- a/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js +++ b/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js @@ -155,6 +155,14 @@ function animationTick(rafTime: number) { } } +function invokerIdleCallbackTimeout(handle: number) { + const callback = idleCallbacks[handle]; + if (callback !== null) { + cancelIdleCallback(handle); + callback(new IdleDeadlineImpl(now(), true)) + } +} + export function requestIdleCallback( callback: IdleRequestCallback, options?: IdleRequestOptions, @@ -165,6 +173,10 @@ export function requestIdleCallback( if (options != null && typeof options.timeout === 'number') { idleCallbackTimeouts[handle] = now() + options.timeout; + window.setTimeout( + () => invokerIdleCallbackTimeout(handle), + options.timeout + ); } if (!isAnimationFrameScheduled) { // If rAF didn't already schedule one, we need to schedule a frame. @@ -177,6 +189,7 @@ export function requestIdleCallback( return 0; } + export function cancelIdleCallback(handle: number) { idleCallbacks[handle] = null; idleCallbackTimeouts[handle] = null; From 7bae7f86917e209705c5ecfcd805fa94504d5310 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Tue, 20 Feb 2018 09:51:34 -0800 Subject: [PATCH 07/14] Implement test suite --- .../src/__tests__/RequestIdleCallback-test.js | 136 +++++++++++++++--- 1 file changed, 119 insertions(+), 17 deletions(-) diff --git a/packages/request-idle-callback-polyfill/src/__tests__/RequestIdleCallback-test.js b/packages/request-idle-callback-polyfill/src/__tests__/RequestIdleCallback-test.js index 7f8daeed48bc7..73e5ea8307774 100644 --- a/packages/request-idle-callback-polyfill/src/__tests__/RequestIdleCallback-test.js +++ b/packages/request-idle-callback-polyfill/src/__tests__/RequestIdleCallback-test.js @@ -3,39 +3,141 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * - * @desc adopted from w3c/web-platform-tests - * @see https://github.com/w3c/web-platform-tests/tree/d977904a0af38c7e2d28c6e1327fb437c2b2e0da/requestidlecallback * * @emails react-core */ 'use strict'; +let highResolutionTimer; let requestIdleCallback; +let cancelIdleCallback; +let animationFrameCallbacks; + +// Overwritten global methods +let previousPostMessage; +let previousRAF; + +function performanceNow() { + return highResolutionTimer; +} + +/** + * A synchronous version of jsdom's postMessage. Meant + * to work with mockRunNextFrame. + */ +function postMessage(message, targetOrign) { + const event = new MessageEvent('message', {data: message}); + event.initEvent('message', false, false); + // MessageEvent.source is defined as read-only and null in jsdom. + // Override the getter so the event.source check doesn't cause early + // returns in idleTick. + Object.defineProperty(event, 'source', { + value: window, + }); + window.dispatchEvent(event); +} + +function requestAnimationFrame(callback) { + animationFrameCallbacks.push(callback); +} + +function mockRunNextFrame() { + const callbacksToRun = animationFrameCallbacks.slice(); + const animationFrameStart = highResolutionTimer++; + animationFrameCallbacks.length = 0; + callbacksToRun.forEach(cb => cb(animationFrameStart)); +} + +function mockLongRunningCode() { + highResolutionTimer += 100; +} describe('RequestIdleCallback', () => { + beforeAll(() => { + // When error supression is enabled, jest is not reporting expect failures + // inside of idle callbacks. + Error.prototype.suppressReactErrorLogging = false; + previousRAF = window.requestAnimationFrame; + previousPostMessage = window.postMessage; + window.postMessage = postMessage; + window.performance = {now: performanceNow}; + window.requestAnimationFrame = requestAnimationFrame; + }); + + afterAll(() => { + window.requestAnimationFrame = previousRAF; + window.postMessage = previousPostMessage; + previousPostMessage = null; + previousRAF = null; + delete window.performance; + }); + beforeEach(() => { + animationFrameCallbacks = []; + highResolutionTimer = 0xf000; jest.resetModules(); - - requestIdleCallback = require('request-idle-callback-polyfill'); + requestIdleCallback = require('request-idle-callback-polyfill') + .requestIdleCallback; + cancelIdleCallback = require('request-idle-callback-polyfill') + .cancelIdleCallback; }); describe('requestIdleCallback', () => { - it('should be a function', () => {}); - it('returns a number', () => {}); - it('exceptions are reported to error handlers', () => {}); - it('nested callbacks get a new idle period', () => {}); - it('nested callbacks dont get the same deadline', () => {}); - it('invoked at least once before the timeout', () => {}); - it('callbacks invoked in order (called iteratively)', () => {}); - it('callbacks invoked in order (called recursively)', () => {}); + it('returns a number', () => { + const callback = jest.fn(); + expect(typeof requestIdleCallback(callback)).toBe('number'); + }); + it('executes callbacks asynchronously', () => { + const callback = jest.fn(); + requestIdleCallback(callback); + expect(callback).not.toBeCalled(); + mockRunNextFrame(); + expect(callback).toBeCalled(); + }); + it('cancels callbacks', () => { + const callback = jest.fn(); + const handle = requestIdleCallback(callback); + cancelIdleCallback(handle); + mockRunNextFrame(); + expect(callback).not.toBeCalled(); + }); + it('passes a deadline to the callback', () => { + const callback = jest.fn(deadline => { + expect(deadline.didTimeout).toBe(false); + expect(deadline.timeRemaining()).toBeGreaterThan(0); + mockLongRunningCode(); + expect(deadline.timeRemaining()).toBe(0); + }); + requestIdleCallback(callback); + mockRunNextFrame(); + expect(callback).toBeCalled(); + }); - }); + it('stops executing callbacks if the deadline expires', () => { + const ops = []; + requestIdleCallback(() => ops.push('first')); + requestIdleCallback(() => { + ops.push('second'); + mockLongRunningCode(); + }); + requestIdleCallback(() => ops.push('third')); + expect(ops).toEqual([]); + mockRunNextFrame(); + expect(ops).toEqual(['first', 'second']); + mockRunNextFrame(); + expect(ops).toEqual(['first', 'second', 'third']); + }); - descibe('cancelIdleCallback', () => { - it('should be a function', () => {}); - it('cancels a callback', () => {}); + it('executes callbacks that timeout', () => { + const callback = jest.fn(deadline => { + expect(deadline.didTimeout).toBe(true); + expect(deadline.timeRemaining()).toBe(0); + }); + requestIdleCallback(callback, {timeout: 100}); + jest.runAllTimers(); + expect(callback).toBeCalled(); + }); }); }); From c6cb7584aad3c46af92bc7a51a9a727cadccafac Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Tue, 20 Feb 2018 09:51:58 -0800 Subject: [PATCH 08/14] Run prettier --- .../npm/index.js | 2 +- .../src/RequestIdleCallback.js | 24 +++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/request-idle-callback-polyfill/npm/index.js b/packages/request-idle-callback-polyfill/npm/index.js index 065e3b02964d3..029729ef50fb7 100644 --- a/packages/request-idle-callback-polyfill/npm/index.js +++ b/packages/request-idle-callback-polyfill/npm/index.js @@ -7,4 +7,4 @@ // module.exports = require('./cjs/{FILE}'); // } -module.exports = require('./cjs/request-idle-callback-polyfill.js'); \ No newline at end of file +module.exports = require('./cjs/request-idle-callback-polyfill.js'); diff --git a/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js b/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js index 827f88ef207f2..e409671d1fc9f 100644 --- a/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js +++ b/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js @@ -31,19 +31,19 @@ const hasNativePerformanceNow = let now; if (hasNativePerformanceNow) { - now = function () { + now = function() { return performance.now(); }; } else { - now = function () { + now = function() { return Date.now(); }; } -function IdleDeadlineImpl(deadline : number, didTimeout: boolean) { +function IdleDeadlineImpl(deadline: number, didTimeout: boolean) { this._deadline = deadline; this.didTimeout = didTimeout; -}; +} IdleDeadlineImpl.prototype.timeRemaining = function() { // If the callback timed out there's definitely no time remaining @@ -54,11 +54,10 @@ IdleDeadlineImpl.prototype.timeRemaining = function() { // gets a performance timer value. Not sure if this is always true. const remaining = this._deadline - now(); return remaining > 0 ? remaining : 0; -} - +}; const idleCallbacks: Array = []; -const idleCallbackTimeouts : Array = []; +const idleCallbackTimeouts: Array = []; let idleCallbackIdentifier = 0; let currentIdleCallbackHandle = 0; let lastIdlePeriodDeadline = 0; @@ -80,7 +79,7 @@ const messageKey = .toString(36) .slice(2); -const idleTick = function (event) { +const idleTick = function(event) { if (event.source !== window || event.data !== messageKey) { return; } @@ -88,7 +87,7 @@ const idleTick = function (event) { isIdleScheduled = false; // While there are still callbacks in the queue... while (currentIdleCallbackHandle < idleCallbacks.length) { - // Get the callback and the timeout, if it exists + // Get the callback and the timeout, if it exists const timeoutTime = idleCallbackTimeouts[currentIdleCallbackHandle]; const callback = idleCallbacks[currentIdleCallbackHandle]; // This callback might have been cancelled, continue to check the rest of the queue @@ -120,7 +119,7 @@ const idleTick = function (event) { didTimeout = false; } currentIdleCallbackHandle++; - callback(new IdleDeadlineImpl(lastIdlePeriodDeadline, didTimeout)); + callback(new IdleDeadlineImpl(lastIdlePeriodDeadline, didTimeout)); } }; // Assumes that we have addEventListener in this environment. Might need @@ -159,7 +158,7 @@ function invokerIdleCallbackTimeout(handle: number) { const callback = idleCallbacks[handle]; if (callback !== null) { cancelIdleCallback(handle); - callback(new IdleDeadlineImpl(now(), true)) + callback(new IdleDeadlineImpl(now(), true)); } } @@ -175,7 +174,7 @@ export function requestIdleCallback( idleCallbackTimeouts[handle] = now() + options.timeout; window.setTimeout( () => invokerIdleCallbackTimeout(handle), - options.timeout + options.timeout, ); } if (!isAnimationFrameScheduled) { @@ -189,7 +188,6 @@ export function requestIdleCallback( return 0; } - export function cancelIdleCallback(handle: number) { idleCallbacks[handle] = null; idleCallbackTimeouts[handle] = null; From 1a0d25ff1fb6dd2ae692c8bfacf01452b11b3b55 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Tue, 20 Feb 2018 09:58:48 -0800 Subject: [PATCH 09/14] Remove scheduledRICCallback This was used when only a single callback was supported --- .../request-idle-callback-polyfill/src/RequestIdleCallback.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js b/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js index e409671d1fc9f..4871c66fbcfcc 100644 --- a/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js +++ b/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js @@ -62,7 +62,6 @@ let idleCallbackIdentifier = 0; let currentIdleCallbackHandle = 0; let lastIdlePeriodDeadline = 0; -let scheduledRICCallback = null; let isIdleScheduled = false; let isAnimationFrameScheduled = false; @@ -166,7 +165,6 @@ export function requestIdleCallback( callback: IdleRequestCallback, options?: IdleRequestOptions, ): number { - scheduledRICCallback = callback; const handle = idleCallbackIdentifier++; idleCallbacks[handle] = callback; From 18a57f4e5ba2b15bc1f81cd2a495811c3efc9cb3 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Wed, 21 Feb 2018 10:58:22 -0800 Subject: [PATCH 10/14] Remove assertions from mock idle callback --- .../src/__tests__/RequestIdleCallback-test.js | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/request-idle-callback-polyfill/src/__tests__/RequestIdleCallback-test.js b/packages/request-idle-callback-polyfill/src/__tests__/RequestIdleCallback-test.js index 73e5ea8307774..79c4e82aa7dee 100644 --- a/packages/request-idle-callback-polyfill/src/__tests__/RequestIdleCallback-test.js +++ b/packages/request-idle-callback-polyfill/src/__tests__/RequestIdleCallback-test.js @@ -55,9 +55,6 @@ function mockLongRunningCode() { describe('RequestIdleCallback', () => { beforeAll(() => { - // When error supression is enabled, jest is not reporting expect failures - // inside of idle callbacks. - Error.prototype.suppressReactErrorLogging = false; previousRAF = window.requestAnimationFrame; previousPostMessage = window.postMessage; window.postMessage = postMessage; @@ -104,15 +101,19 @@ describe('RequestIdleCallback', () => { }); it('passes a deadline to the callback', () => { + const ops = []; const callback = jest.fn(deadline => { - expect(deadline.didTimeout).toBe(false); - expect(deadline.timeRemaining()).toBeGreaterThan(0); + ops.push(deadline.didTimeout); + ops.push(deadline.timeRemaining()); mockLongRunningCode(); - expect(deadline.timeRemaining()).toBe(0); + ops.push(deadline.timeRemaining()); }); requestIdleCallback(callback); mockRunNextFrame(); expect(callback).toBeCalled(); + expect(ops[0]).toBe(false); + expect(ops[1]).toBeGreaterThan(0); + expect(ops[2]).toBe(0); }); it('stops executing callbacks if the deadline expires', () => { @@ -131,13 +132,14 @@ describe('RequestIdleCallback', () => { }); it('executes callbacks that timeout', () => { + const ops = []; const callback = jest.fn(deadline => { - expect(deadline.didTimeout).toBe(true); - expect(deadline.timeRemaining()).toBe(0); + ops.push(deadline.didTimeout); + ops.push(deadline.timeRemaining()); }); requestIdleCallback(callback, {timeout: 100}); jest.runAllTimers(); - expect(callback).toBeCalled(); + expect(ops).toEqual([true, 0]); }); }); }); From 299737d283d8628bcb2ff80c6c607d8635270597 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Wed, 21 Feb 2018 11:08:01 -0800 Subject: [PATCH 11/14] Export a PROD and DEV build --- .../request-idle-callback-polyfill/npm/index.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/request-idle-callback-polyfill/npm/index.js b/packages/request-idle-callback-polyfill/npm/index.js index 029729ef50fb7..69511615332ab 100644 --- a/packages/request-idle-callback-polyfill/npm/index.js +++ b/packages/request-idle-callback-polyfill/npm/index.js @@ -1,10 +1,7 @@ 'use strict'; -// @TODO figure out if we need a prod/dev build for this? -// if (process.env.NODE_ENV === 'production') { -// module.exports = require('./cjs/{FILE}'); -// } else { -// module.exports = require('./cjs/{FILE}'); -// } - -module.exports = require('./cjs/request-idle-callback-polyfill.js'); +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/request-idle-callback-polyfill.production.min.js'); +} else { + module.exports = require('./cjs/request-idle-callback-polyfill.development.js'); +} From cd1bfc7d7e01b70db5783c53773670342cc00e33 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Wed, 21 Feb 2018 13:11:18 -0800 Subject: [PATCH 12/14] Include cjs/ in package.json file --- packages/request-idle-callback-polyfill/package.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/request-idle-callback-polyfill/package.json b/packages/request-idle-callback-polyfill/package.json index f5823900c99de..5adbabb34096f 100644 --- a/packages/request-idle-callback-polyfill/package.json +++ b/packages/request-idle-callback-polyfill/package.json @@ -8,11 +8,7 @@ "homepage": "https://facebook.github.io/react/", "bugs": "https://github.com/facebook/react/issues", "license": "MIT", - "files": [ - "LICENSE", - "README.md", - "index.js" - ], + "files": ["LICENSE", "README.md", "index.js", "cjs/"], "main": "index.js", "repository": "facebook/react", "dependencies": { From 96e115628245ec854e95632e0d9a33b95f3e4543 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Wed, 21 Feb 2018 14:28:31 -0800 Subject: [PATCH 13/14] Lazily register message listener While it's reasonable for the polyfill to assume a DOM environment, ReactDOM itself must be able to run in a Node environment. These polyfills won't actually be called if it is, so making any DOM-specific initialization lazy protects us from breaking Node. --- .../src/RequestIdleCallback.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js b/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js index 4871c66fbcfcc..34b1679a0ac52 100644 --- a/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js +++ b/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js @@ -70,6 +70,9 @@ let isAnimationFrameScheduled = false; // frames. let previousFrameTime = 33; let activeFrameTime = 33; +// Tracks whether the 'message' event listener has been registered, which is done +// lazily the first time requestIdleCallback is called +let registeredMessageListener = false; // We use the postMessage trick to defer idle work until after the repaint. const messageKey = @@ -121,9 +124,6 @@ const idleTick = function(event) { callback(new IdleDeadlineImpl(lastIdlePeriodDeadline, didTimeout)); } }; -// Assumes that we have addEventListener in this environment. Might need -// something better for old IE. -window.addEventListener('message', idleTick, false); function animationTick(rafTime: number) { isAnimationFrameScheduled = false; @@ -175,6 +175,14 @@ export function requestIdleCallback( options.timeout, ); } + + // Lazily register the listener when rIC is first called + if (!registeredMessageListener) { + // Assumes that we have addEventListener in this environment. Might need + // something better for old IE. + window.addEventListener('message', idleTick, false); + registeredMessageListener = true; + } 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 From b3a5223d27e2c6ffe4503c353145ea8efb9afdd4 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Wed, 21 Feb 2018 14:30:45 -0800 Subject: [PATCH 14/14] Use request-idle-callback polyfill in ReactDOMFrameScheduling --- .../src/RequestIdleCallback.js | 2 +- packages/shared/ReactDOMFrameScheduling.js | 161 ++---------------- 2 files changed, 12 insertions(+), 151 deletions(-) diff --git a/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js b/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js index 34b1679a0ac52..b8be9ba3f49ac 100644 --- a/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js +++ b/packages/request-idle-callback-polyfill/src/RequestIdleCallback.js @@ -24,7 +24,7 @@ type IdleRequestOptions = { timeout: number, }; -type IdleRequestCallback = IdleDeadline => void; +export type IdleRequestCallback = IdleDeadline => void; const hasNativePerformanceNow = typeof performance === 'object' && typeof performance.now === 'function'; diff --git a/packages/shared/ReactDOMFrameScheduling.js b/packages/shared/ReactDOMFrameScheduling.js index 86ddc9360f84a..8d2bf33bdd0e2 100644 --- a/packages/shared/ReactDOMFrameScheduling.js +++ b/packages/shared/ReactDOMFrameScheduling.js @@ -15,8 +15,12 @@ // layout, paint and other browser work is counted against the available time. // The frame rate is dynamically adjusted. -import type {Deadline} from 'react-reconciler'; +import type {IdleRequestCallback} from 'request-idle-callback-polyfill'; +import { + requestIdleCallback, + cancelIdleCallback, +} from 'request-idle-callback-polyfill'; import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'; import warning from 'fbjs/lib/warning'; @@ -47,18 +51,14 @@ if (hasNativePerformanceNow) { }; } -// TODO: There's no way to cancel, because Fiber doesn't atm. -let rIC: ( - callback: (deadline: Deadline, options?: {timeout: number}) => void, -) => number; -let cIC: (callbackID: number) => void; +let rIC: requestIdleCallback; +let cIC: cancelIdleCallback; if (!ExecutionEnvironment.canUseDOM) { - rIC = function( - frameCallback: (deadline: Deadline, options?: {timeout: number}) => void, - ): number { + rIC = function(frameCallback: IdleRequestCallback): number { return setTimeout(() => { frameCallback({ + didTimeout: false, timeRemaining() { return Infinity; }, @@ -72,147 +72,8 @@ if (!ExecutionEnvironment.canUseDOM) { typeof requestIdleCallback !== 'function' || typeof cancelIdleCallback !== 'function' ) { - // Polyfill requestIdleCallback and cancelIdleCallback - - let scheduledRICCallback = null; - let isIdleScheduled = false; - let timeoutTime = -1; - - let isAnimationFrameScheduled = false; - - let frameDeadline = 0; - // We start out assuming that we run at 30fps but then the heuristic tracking - // will adjust this value to a faster fps if we get more frequent animation - // frames. - let previousFrameTime = 33; - let activeFrameTime = 33; - - let frameDeadlineObject; - if (hasNativePerformanceNow) { - frameDeadlineObject = { - didTimeout: false, - timeRemaining() { - // We assume that if we have a performance timer that the rAF callback - // gets a performance timer value. Not sure if this is always true. - const remaining = frameDeadline - performance.now(); - return remaining > 0 ? remaining : 0; - }, - }; - } else { - frameDeadlineObject = { - didTimeout: false, - timeRemaining() { - // Fallback to Date.now() - const remaining = frameDeadline - Date.now(); - return remaining > 0 ? remaining : 0; - }, - }; - } - - // We use the postMessage trick to defer idle work until after the repaint. - const messageKey = - '__reactIdleCallback$' + - Math.random() - .toString(36) - .slice(2); - const idleTick = function(event) { - if (event.source !== window || event.data !== messageKey) { - return; - } - - isIdleScheduled = false; - - const currentTime = now(); - if (frameDeadline - currentTime <= 0) { - // There's no time left in this idle period. Check if the callback has - // a timeout and whether it's been exceeded. - if (timeoutTime !== -1 && timeoutTime <= currentTime) { - // Exceeded the timeout. Invoke the callback even though there's no - // time left. - frameDeadlineObject.didTimeout = true; - } else { - // No timeout. - if (!isAnimationFrameScheduled) { - // Schedule another animation callback so we retry later. - isAnimationFrameScheduled = true; - requestAnimationFrame(animationTick); - } - // Exit without invoking the callback. - return; - } - } else { - // There's still time left in this idle period. - frameDeadlineObject.didTimeout = false; - } - - timeoutTime = -1; - const callback = scheduledRICCallback; - scheduledRICCallback = null; - if (callback !== null) { - callback(frameDeadlineObject); - } - }; - // Assumes that we have addEventListener in this environment. Might need - // something better for old IE. - window.addEventListener('message', idleTick, false); - - const animationTick = function(rafTime) { - isAnimationFrameScheduled = false; - let nextFrameTime = rafTime - frameDeadline + activeFrameTime; - if ( - nextFrameTime < activeFrameTime && - previousFrameTime < activeFrameTime - ) { - if (nextFrameTime < 8) { - // Defensive coding. We don't support higher frame rates than 120hz. - // If we get lower than that, it is probably a bug. - nextFrameTime = 8; - } - // If one frame goes long, then the next one can be short to catch up. - // If two frames are short in a row, then that's an indication that we - // actually have a higher frame rate than what we're currently optimizing. - // We adjust our heuristic dynamically accordingly. For example, if we're - // running on 120hz display or 90hz VR display. - // Take the max of the two in case one of them was an anomaly due to - // missed frame deadlines. - activeFrameTime = - nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime; - } else { - previousFrameTime = nextFrameTime; - } - frameDeadline = rafTime + activeFrameTime; - if (!isIdleScheduled) { - isIdleScheduled = true; - window.postMessage(messageKey, '*'); - } - }; - - rIC = function( - callback: (deadline: Deadline) => void, - options?: {timeout: number}, - ): number { - // This assumes that we only schedule one callback at a time because that's - // how Fiber uses it. - scheduledRICCallback = callback; - if (options != null && typeof options.timeout === 'number') { - timeoutTime = now() + options.timeout; - } - 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 - // might want to still have setTimeout trigger rIC as a backup to ensure - // that we keep performing work. - isAnimationFrameScheduled = true; - requestAnimationFrame(animationTick); - } - return 0; - }; - - cIC = function() { - scheduledRICCallback = null; - isIdleScheduled = false; - timeoutTime = -1; - }; + rIC = requestIdleCallback; + cIC = cancelIdleCallback; } else { rIC = window.requestIdleCallback; cIC = window.cancelIdleCallback;