Skip to content

Commit 076bbea

Browse files
authored
Fall back to 'setTimeout' when 'requestAnimationFrame' is not called (#13091)
* Add fixture test for schedule running when tab is backgrounded **what is the change?:** Just adding a test to the fixture, where we can easily see whether scheduled callbacks are called after switching away from the fixture tab. **why make this change?:** We are about to fix the schedule module so that it still runs even when the tab is in the backround. **test plan:** Manually tested the fixture, verified that it works as expected and right now callbacks are not called when the tab is in the background. **issue:** Internal task T30754186 * Fall back to 'setTimeout' when 'requestAnimationFrame' is not called **what is the change?:** If 'requestAnimationFrame' is not called for 100ms we fall back to 'setTimeout' to schedule the postmessage. **why make this change?:** When you start loading a page, and then switch tabs, 'requestAnimationFrame' is throttled or not called until you come back to that tab. That means React's rendering, any any other scheduled work, are paused. Users expect the page to continue loading, and rendering is part of the page load in a React app. So we need to continue calling callbacks. **test plan:** Manually tested using the new fixture test, observed that the callbacks were called while switched to another tab. They were called more slowly, but that seems like a reasonable thing. **issue:** Internal task T30754186 * make arguments more explicit
1 parent da5c87b commit 076bbea

File tree

2 files changed

+50
-3
lines changed

2 files changed

+50
-3
lines changed

fixtures/schedule/index.html

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ <h2>Tests:</h2>
2323
<ol>
2424
<li>
2525
<button onClick="runTestOne()">Run Test 1</button>
26-
<p>Calls the callback with the frame when not blocked:</p>
26+
<p>Calls the callback within the frame when not blocked:</p>
2727
<div><b>Expected:</b></div>
2828
<div id="test-1-expected">
2929
</div>
@@ -79,6 +79,15 @@ <h2>Tests:</h2>
7979
<p><b>IMPORTANT:</b> Open the console when you run this! Inspect the logs there!</p>
8080
<button onClick="runTestSix()">Run Test 6</button>
8181
</li>
82+
<li>
83+
<p>Continues calling callbacks even when user switches away from this tab</p>
84+
<button onClick="runTestSeven()">Run Test 7</button>
85+
<div><b>Click the button above, observe the counter, then switch to
86+
another tab and switch back:</b></div>
87+
<div id="test-7">
88+
</div>
89+
<div> If the counter advanced while you were away from this tab, it's correct.</div>
90+
</li>
8291
</ol>
8392
<script src="../../build/dist/react-scheduler.development.js"></script>
8493
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
@@ -464,6 +473,22 @@ <h2>Tests:</h2>
464473
console.log('scheduled cbE');
465474
};
466475
}
476+
477+
function runTestSeven() {
478+
// Test 7
479+
// Calls callbacks, continues calling them even when this tab is in the
480+
// background
481+
clearTestResult(7);
482+
let counter = -1;
483+
function incrementCounterAndScheduleNextCallback() {
484+
const counterNode = document.getElementById('test-7');
485+
counter++;
486+
counterNode.innerHTML = counter;
487+
waitForTimeToPass(100);
488+
scheduleWork(incrementCounterAndScheduleNextCallback);
489+
}
490+
scheduleWork(incrementCounterAndScheduleNextCallback);
491+
}
467492
</script type="text/babel">
468493
</body>
469494
</html>

packages/react-scheduler/src/ReactScheduler.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ if (!canUseDOM) {
117117
};
118118
} else {
119119
const localRequestAnimationFrame = requestAnimationFrame;
120+
const localCancelAnimationFrame = cancelAnimationFrame;
120121

121122
let headOfPendingCallbacksLinkedList: CallbackConfigType | null = null;
122123
let tailOfPendingCallbacksLinkedList: CallbackConfigType | null = null;
@@ -128,6 +129,27 @@ if (!canUseDOM) {
128129
let isIdleScheduled = false;
129130
let isAnimationFrameScheduled = false;
130131

132+
// requestAnimationFrame does not run when the tab is in the background.
133+
// if we're backgrounded we prefer for that work to happen so that the page
134+
// continues to load in the background.
135+
// so we also schedule a 'setTimeout' as a fallback.
136+
const animationFrameTimeout = 100;
137+
let rafID;
138+
let timeoutID;
139+
const scheduleAnimationFrameWithFallbackSupport = function(callback) {
140+
// schedule rAF and also a setTimeout
141+
rafID = localRequestAnimationFrame(function(timestamp) {
142+
// cancel the setTimeout
143+
localClearTimeout(timeoutID);
144+
callback(timestamp);
145+
});
146+
timeoutID = localSetTimeout(function() {
147+
// cancel the requestAnimationFrame
148+
localCancelAnimationFrame(rafID);
149+
callback(now());
150+
}, animationFrameTimeout);
151+
};
152+
131153
let frameDeadline = 0;
132154
// We start out assuming that we run at 30fps but then the heuristic tracking
133155
// will adjust this value to a faster fps if we get more frequent animation
@@ -266,7 +288,7 @@ if (!canUseDOM) {
266288
if (!isAnimationFrameScheduled) {
267289
// Schedule another animation callback so we retry later.
268290
isAnimationFrameScheduled = true;
269-
localRequestAnimationFrame(animationTick);
291+
scheduleAnimationFrameWithFallbackSupport(animationTick);
270292
}
271293
}
272294
};
@@ -347,7 +369,7 @@ if (!canUseDOM) {
347369
// might want to still have setTimeout trigger scheduleWork as a backup to ensure
348370
// that we keep performing work.
349371
isAnimationFrameScheduled = true;
350-
localRequestAnimationFrame(animationTick);
372+
scheduleAnimationFrameWithFallbackSupport(animationTick);
351373
}
352374
return scheduledCallbackConfig;
353375
};

0 commit comments

Comments
 (0)