From 076bbeace7e089ee748a0b59466df654305bbe2b Mon Sep 17 00:00:00 2001 From: Flarnie Marchan Date: Fri, 22 Jun 2018 09:13:47 -0700 Subject: [PATCH] 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 --- fixtures/schedule/index.html | 27 ++++++++++++++++++- .../react-scheduler/src/ReactScheduler.js | 26 ++++++++++++++++-- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/fixtures/schedule/index.html b/fixtures/schedule/index.html index d5b243a3c2..ad5f1042f5 100644 --- a/fixtures/schedule/index.html +++ b/fixtures/schedule/index.html @@ -23,7 +23,7 @@
  1. -

    Calls the callback with the frame when not blocked:

    +

    Calls the callback within the frame when not blocked:

    Expected:
    @@ -79,6 +79,15 @@

    IMPORTANT: Open the console when you run this! Inspect the logs there!

  2. +
  3. +

    Continues calling callbacks even when user switches away from this tab

    + +
    Click the button above, observe the counter, then switch to + another tab and switch back:
    +
    +
    +
    If the counter advanced while you were away from this tab, it's correct.
    +
@@ -464,6 +473,22 @@ function runTestSix() { console.log('scheduled cbE'); }; } + +function runTestSeven() { + // Test 7 + // Calls callbacks, continues calling them even when this tab is in the + // background + clearTestResult(7); + let counter = -1; + function incrementCounterAndScheduleNextCallback() { + const counterNode = document.getElementById('test-7'); + counter++; + counterNode.innerHTML = counter; + waitForTimeToPass(100); + scheduleWork(incrementCounterAndScheduleNextCallback); + } + scheduleWork(incrementCounterAndScheduleNextCallback); +} diff --git a/packages/react-scheduler/src/ReactScheduler.js b/packages/react-scheduler/src/ReactScheduler.js index 2b016f1275..79268af395 100644 --- a/packages/react-scheduler/src/ReactScheduler.js +++ b/packages/react-scheduler/src/ReactScheduler.js @@ -117,6 +117,7 @@ if (!canUseDOM) { }; } else { const localRequestAnimationFrame = requestAnimationFrame; + const localCancelAnimationFrame = cancelAnimationFrame; let headOfPendingCallbacksLinkedList: CallbackConfigType | null = null; let tailOfPendingCallbacksLinkedList: CallbackConfigType | null = null; @@ -128,6 +129,27 @@ if (!canUseDOM) { let isIdleScheduled = false; let isAnimationFrameScheduled = false; + // requestAnimationFrame does not run when the tab is in the background. + // if we're backgrounded we prefer for that work to happen so that the page + // continues to load in the background. + // so we also schedule a 'setTimeout' as a fallback. + const animationFrameTimeout = 100; + let rafID; + let timeoutID; + const scheduleAnimationFrameWithFallbackSupport = function(callback) { + // schedule rAF and also a setTimeout + rafID = localRequestAnimationFrame(function(timestamp) { + // cancel the setTimeout + localClearTimeout(timeoutID); + callback(timestamp); + }); + timeoutID = localSetTimeout(function() { + // cancel the requestAnimationFrame + localCancelAnimationFrame(rafID); + callback(now()); + }, animationFrameTimeout); + }; + 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 @@ -266,7 +288,7 @@ if (!canUseDOM) { if (!isAnimationFrameScheduled) { // Schedule another animation callback so we retry later. isAnimationFrameScheduled = true; - localRequestAnimationFrame(animationTick); + scheduleAnimationFrameWithFallbackSupport(animationTick); } } }; @@ -347,7 +369,7 @@ if (!canUseDOM) { // might want to still have setTimeout trigger scheduleWork as a backup to ensure // that we keep performing work. isAnimationFrameScheduled = true; - localRequestAnimationFrame(animationTick); + scheduleAnimationFrameWithFallbackSupport(animationTick); } return scheduledCallbackConfig; };