Files
react/packages/shared/ReactDOMFrameScheduling.js
Andrew Clark 9b36df86c6 Use requestIdleCallback timeout to force expiration (#11548)
* Don't call idle callback unless there's time remaining

* Expiration fixture

Fixture that demonstrates how async work expires after a certain interval.
The fixture clogs the main thread with animation work, so it only works if the
`timeout` option is provided to `requestIdleCallback`.

* Pass timeout option to requestIdleCallback

Forces `requestIdleCallback` to fire if too much time has elapsed, even if the
main thread is busy. Required to make expiration times work properly. Otherwise,
async work can expire, but React never has a chance to flush it because the
browser never calls into React.
2017-11-15 13:46:17 -08:00

222 lines
6.8 KiB
JavaScript

/**
* 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 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. http://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 (
typeof requestIdleCallback !== 'function' ||
typeof cancelIdleCallback !== 'function'
) {
// Polyfill requestIdleCallback and cancelIdleCallback
var scheduledRICCallback = null;
var isIdleScheduled = false;
var timeoutTime = -1;
var isAnimationFrameScheduled = false;
var 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.
var previousFrameTime = 33;
var activeFrameTime = 33;
var 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.
var messageKey =
'__reactIdleCallback$' +
Math.random()
.toString(36)
.slice(2);
var 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;
var 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);
var animationTick = function(rafTime) {
isAnimationFrameScheduled = false;
var 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};