Skip to content

Commit 287acc7

Browse files
rickhanloniityao1
authored andcommitted
Re-use rAF, don't re-schedule it
1 parent 238402d commit 287acc7

File tree

4 files changed

+100
-30
lines changed

4 files changed

+100
-30
lines changed

packages/react-dom-bindings/src/client/ReactDOMHostConfig.js

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -414,37 +414,59 @@ export const scheduleMicrotask: any =
414414
.catch(handleErrorInNextTick)
415415
: scheduleTimeout; // TODO: Determine the best fallback here.
416416

417-
// -------------------
418-
// requestAnimationFrame
419-
// -------------------
420-
type FrameAlignedTask = {
421-
frameNode: any,
422-
callbackNode: any,
423-
};
424-
425417
// TODO: Fix these types
426418
export const supportsFrameAlignedTask = true;
427-
export function scheduleFrameAlignedTask(task: any): FrameAlignedTask {
428-
// Schedule both tasks, we'll race them and use the first to fire.
429-
const raf: any = localRequestAnimationFrame;
430419

431-
return {
432-
frameNode: raf(task),
433-
callbackNode: Scheduler.unstable_scheduleCallback(
420+
type FrameAlignedTask = {|
421+
rafNode: number,
422+
schedulerNode: number,
423+
task: function,
424+
|};
425+
426+
let currentTask: FrameAlignedTask | null = null;
427+
function performFrameAlignedWork() {
428+
if (currentTask != null) {
429+
const task = currentTask.task;
430+
localCancelAnimationFrame(currentTask.id);
431+
Scheduler.unstable_cancelCallback(currentTask.schedulerNode);
432+
currentTask = null;
433+
if (task != null) {
434+
task();
435+
}
436+
}
437+
}
438+
439+
export function scheduleFrameAlignedTask(task: any): any {
440+
if (currentTask === null) {
441+
const rafNode = localRequestAnimationFrame(performFrameAlignedWork);
442+
443+
const schedulerNode = Scheduler.unstable_scheduleCallback(
434444
Scheduler.unstable_NormalPriority,
445+
performFrameAlignedWork,
446+
);
447+
448+
currentTask = {
449+
rafNode,
450+
schedulerNode,
435451
task,
436-
),
437-
};
438-
}
439-
export function cancelFrameAlignedTask(task: any) {
440-
const caf: any = localCancelAnimationFrame;
441-
if (task.frameNode != null) {
442-
caf(task.frameNode);
452+
};
453+
} else {
454+
currentTask.task = task;
455+
currentTask.schedulerNode = Scheduler.unstable_scheduleCallback(
456+
Scheduler.unstable_NormalPriority,
457+
performFrameAlignedWork,
458+
);
443459
}
444460

445-
if (task.callbackNode != null) {
446-
Scheduler.unstable_cancelCallback(task.callbackNode);
447-
}
461+
return currentTask;
462+
}
463+
464+
export function cancelFrameAlignedTask(task: FrameAlignedTask) {
465+
Scheduler.unstable_cancelCallback(task.schedulerNode);
466+
task.schedulerNode = null;
467+
// We don't cancel the rAF in case it gets re-used later.
468+
// But clear the task so if it fires and shouldn't run, it won't.
469+
task.task = null;
448470
}
449471

450472
function handleErrorInNextTick(error) {

packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,44 @@ describe('ReactDOMFiberAsync', () => {
618618
expect(counterRef.current.textContent).toBe('Count: 2');
619619
});
620620

621+
// @gate enableFrameEndScheduling
622+
it('Should re-use scheduled rAF, not cancel and schedule anew', () => {
623+
let setState = null;
624+
let counterRef = null;
625+
function Counter() {
626+
const [count, setCount] = React.useState(0);
627+
const ref = React.useRef();
628+
setState = setCount;
629+
counterRef = ref;
630+
Scheduler.unstable_yieldValue('Count: ' + count);
631+
return <p ref={ref}>Count: {count}</p>;
632+
}
633+
634+
const root = ReactDOMClient.createRoot(container);
635+
act(() => {
636+
root.render(<Counter />);
637+
});
638+
expect(Scheduler).toHaveYielded(['Count: 0']);
639+
640+
window.event = undefined;
641+
setState(1);
642+
// Unknown updates should schedule a rAF.
643+
expect(global.requestAnimationFrameQueue.length).toBe(1);
644+
const firstRaf = global.requestAnimationFrameQueue[0];
645+
646+
setState(2);
647+
// Default updates after unknown should re-use the scheduled rAF.
648+
expect(global.requestAnimationFrameQueue.length).toBe(1);
649+
const secondRaf = global.requestAnimationFrameQueue[0];
650+
expect(firstRaf).toBe(secondRaf);
651+
652+
expect(Scheduler).toHaveYielded([]);
653+
expect(counterRef.current.textContent).toBe('Count: 0');
654+
global.flushRequestAnimationFrameQueue();
655+
expect(Scheduler).toHaveYielded(['Count: 2']);
656+
expect(counterRef.current.textContent).toBe('Count: 2');
657+
});
658+
621659
// @gate enableFrameEndScheduling
622660
it('Default update followed by an unknown update is batched, scheduled in a rAF', () => {
623661
let setState = null;

packages/react-reconciler/src/ReactFiberWorkLoop.new.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -925,9 +925,13 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
925925
if (
926926
enableFrameEndScheduling &&
927927
newCallbackPriority === DefaultLane &&
928+
existingCallbackNode !== null &&
929+
// TODO: We can't expose the rafNode here,
930+
// but how do we know the rAF is not scheduled?
931+
existingCallbackNode.rafNode == null &&
928932
root.hasUnknownUpdates
929933
) {
930-
// Do nothing, we need to cancel the existing default task and schedule a rAF.
934+
// Do nothing, we need to schedule a new rAF.
931935
} else {
932936
// The priority hasn't changed. We can reuse the existing task. Exit.
933937
return;
@@ -940,8 +944,9 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
940944
enableFrameEndScheduling &&
941945
supportsFrameAlignedTask &&
942946
existingCallbackNode != null &&
943-
// TODO: is there a better check for callbackNode type?
944-
existingCallbackNode.frameNode != null
947+
// TODO: we can't expose the scheduler node here,
948+
// but how do we know we need to cancel with the host config method?
949+
existingCallbackNode.schedulerNode != null
945950
) {
946951
cancelFrameAlignedTask(existingCallbackNode);
947952
} else {

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -925,9 +925,13 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
925925
if (
926926
enableFrameEndScheduling &&
927927
newCallbackPriority === DefaultLane &&
928+
existingCallbackNode !== null &&
929+
// TODO: We can't expose the rafNode here,
930+
// but how do we know the rAF is not scheduled?
931+
existingCallbackNode.rafNode == null &&
928932
root.hasUnknownUpdates
929933
) {
930-
// Do nothing, we need to cancel the existing default task and schedule a rAF.
934+
// Do nothing, we need to schedule a new rAF.
931935
} else {
932936
// The priority hasn't changed. We can reuse the existing task. Exit.
933937
return;
@@ -940,8 +944,9 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
940944
enableFrameEndScheduling &&
941945
supportsFrameAlignedTask &&
942946
existingCallbackNode != null &&
943-
// TODO: is there a better check for callbackNode type?
944-
existingCallbackNode.frameNode != null
947+
// TODO: we can't expose the scheduler node here,
948+
// but how do we know we need to cancel with the host config method?
949+
existingCallbackNode.schedulerNode != null
945950
) {
946951
cancelFrameAlignedTask(existingCallbackNode);
947952
} else {

0 commit comments

Comments
 (0)