/** * 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. import type {Deadline} from 'react-reconciler'; import {alwaysUseRequestIdleCallbackPolyfill} from 'shared/ReactFeatureFlags'; import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'; import warning from 'fbjs/lib/warning'; if (__DEV__) { if ( ExecutionEnvironment.canUseDOM && typeof requestAnimationFrame !== 'function' ) { warning( false, 'React depends on requestAnimationFrame. Make sure that you load a ' + 'polyfill in older browsers. https://fb.me/react-polyfills', ); } } const hasNativePerformanceNow = typeof performance === 'object' && typeof performance.now === 'function'; let now; if (hasNativePerformanceNow) { now = function() { return performance.now(); }; } else { now = function() { return Date.now(); }; } // 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; if (!ExecutionEnvironment.canUseDOM) { rIC = function( frameCallback: (deadline: Deadline, options?: {timeout: number}) => void, ): number { return setTimeout(() => { frameCallback({ timeRemaining() { return Infinity; }, }); }); }; cIC = function(timeoutID: number) { clearTimeout(timeoutID); }; } else if ( alwaysUseRequestIdleCallbackPolyfill || 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; }; } else { rIC = window.requestIdleCallback; cIC = window.cancelIdleCallback; } export {now, rIC, cIC};