Skip to content

Commit

Permalink
[scheduler] Eagerly schedule rAF at beginning of frame
Browse files Browse the repository at this point in the history
Eagerly schedule the next animation callback at the beginning of the
frame. If the scheduler queue is not empty at the end of the frame, it
will continue flushing inside that callback. If the queue *is* empty,
then it will exit immediately. Posting the callback at the start of the
frame ensures it's fired within the earliest possible frame. If we
waited until the end of the frame to post the callback, we risk the
browser skipping a frame and not firing the callback until the frame
after that.
  • Loading branch information
acdlite committed Oct 6, 2018
1 parent d836010 commit 0699ef3
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 4 deletions.
17 changes: 16 additions & 1 deletion packages/scheduler/src/Scheduler.js
Expand Up @@ -605,7 +605,22 @@ if (typeof window !== 'undefined' && window._schedMock) {
window.addEventListener('message', idleTick, false);

var animationTick = function(rafTime) {
isAnimationFrameScheduled = false;
if (scheduledCallback !== null) {
// Eagerly schedule the next animation callback at the beginning of the
// frame. If the scheduler queue is not empty at the end of the frame, it
// will continue flushing inside that callback. If the queue *is* empty,
// then it will exit immediately. Posting the callback at the start of the
// frame ensures it's fired within the earliest possible frame. If we
// waited until the end of the frame to post the callback, we risk the
// browser skipping a frame and not firing the callback until the frame
// after that.
requestAnimationFrameWithTimeout(animationTick);
} else {
// No pending work. Exit.
isAnimationFrameScheduled = false;
return;
}

var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
if (
nextFrameTime < activeFrameTime &&
Expand Down
26 changes: 23 additions & 3 deletions packages/scheduler/src/__tests__/SchedulerDOM-test.js
Expand Up @@ -50,17 +50,18 @@ describe('SchedulerDOM', () => {
function runRAFCallbacks() {
startOfLatestFrame += frameSize;
currentTime = startOfLatestFrame;
rAFCallbacks.forEach(cb => cb());
const cbs = rAFCallbacks;
rAFCallbacks = [];
cbs.forEach(cb => cb());
}
function advanceOneFrame(config: FrameTimeoutConfigType = {}) {
runRAFCallbacks();
runPostMessageCallbacks(config);
}

let frameSize = 33;
let startOfLatestFrame = Date.now();
let currentTime = Date.now();
let startOfLatestFrame = 0;
let currentTime = 0;

beforeEach(() => {
// TODO pull this into helper method, reduce repetition.
Expand Down Expand Up @@ -109,6 +110,25 @@ describe('SchedulerDOM', () => {
expect(typeof cb.mock.calls[0][0].timeRemaining()).toBe('number');
});

it('inserts its rAF callback as early into the queue as possible', () => {
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const log = [];
const useRAFCallback = () => {
log.push('userRAFCallback');
};
scheduleCallback(() => {
// Call rAF while idle work is being flushed.
requestAnimationFrame(useRAFCallback);
});
advanceOneFrame({timeLeftInFrame: 1});
// There should be two callbacks: the one scheduled by Scheduler at the
// beginning of the frame, and the one scheduled later during that frame.
expect(rAFCallbacks.length).toBe(2);
// The user callback should be the second callback.
rAFCallbacks[1]();
expect(log).toEqual(['userRAFCallback']);
});

describe('with multiple callbacks', () => {
it('accepts multiple callbacks and calls within frame when not blocked', () => {
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
Expand Down

0 comments on commit 0699ef3

Please sign in to comment.