[Scheduler] requestPaint (#15960)
* [Scheduler] requestPaint Signals to Scheduler that the browser needs to paint the screen. React will call it in the commit phase. Scheduler will yield at the end of the current frame, even if there is no pending input. When `isInputPending` is not available, this has no effect, because we yield at the end of every frame regardless. React will call `requestPaint` in the commit phase as long as there's at least one effect. We could choose not to call it if none of the effects are DOM mutations, but this is so rare that it doesn't seem worthwhile to bother checking. * Fall back gracefully if requestPaint is missing
Andrew Clark committed
Jun 22, 2019 at 00:15 UTC
6568a79931bb335f2c997ed9efbc533f2677df5c
13 files changed
+119
-35
packages/react-art/src/ReactARTHostConfig.js
-12
@@ -7,23 +7,11 @@
7
8
import Transform from 'art/core/transform';
9
import Mode from 'art/modes/current';
10
-import * as Scheduler from 'scheduler';
10
import invariant from 'shared/invariant';
11
12
import {TYPES, EVENT_TYPES, childrenAsString} from './ReactARTInternals';
13
import type {ReactEventComponentInstance} from 'shared/ReactTypes';
14
16
-// Intentionally not named imports because Rollup would
17
-// use dynamic dispatch for CommonJS interop named imports.
18
-const {
19
- unstable_now: now,
20
- unstable_scheduleCallback: scheduleDeferredCallback,
21
- unstable_shouldYield: shouldYield,
22
- unstable_cancelCallback: cancelDeferredCallback,
23
-} = Scheduler;
24
-
25
-export {now, scheduleDeferredCallback, shouldYield, cancelDeferredCallback};
26
-
15
const pooledTransform = new Transform();
16
17
const NO_CONTEXT = {};
packages/react-dom/src/client/ReactDOMHostConfig.js
-13
@@ -7,8 +7,6 @@
7
* @flow
8
*/
9
10
-import * as Scheduler from 'scheduler';
11
-
10
import {precacheFiberNode, updateFiberProps} from './ReactDOMComponentTree';
11
import {
12
createElement,
@@ -113,17 +111,6 @@ import warning from 'shared/warning';
111
112
const {html: HTML_NAMESPACE} = Namespaces;
113
116
-// Intentionally not named imports because Rollup would
117
-// use dynamic dispatch for CommonJS interop named imports.
118
-const {
119
- unstable_now: now,
120
- unstable_scheduleCallback: scheduleDeferredCallback,
121
- unstable_shouldYield: shouldYield,
122
- unstable_cancelCallback: cancelDeferredCallback,
123
-} = Scheduler;
124
-
125
-export {now, scheduleDeferredCallback, shouldYield, cancelDeferredCallback};
126
-
114
let SUPPRESS_HYDRATION_WARNING;
115
if (__DEV__) {
116
SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';
packages/react-reconciler/src/ReactFiberWorkLoop.js
+5
@@ -36,6 +36,7 @@ import {
36
getCurrentPriorityLevel,
37
runWithPriority,
38
shouldYield,
39
+ requestPaint,
40
now,
41
ImmediatePriority,
42
UserBlockingPriority,
@@ -1666,6 +1667,10 @@ function commitRootImpl(root) {
1667
1668
nextEffect = null;
1669
1670
+ // Tell Scheduler to yield at the end of the frame, so the browser has an
1671
+ // opportunity to paint.
1672
+ requestPaint();
1673
+
1674
if (enableSchedulerTracing) {
1675
__interactionsRef.current = ((prevInteractions: any): Set<Interaction>);
1676
}
packages/react-reconciler/src/SchedulerWithReactIntegration.js
+4
@@ -19,6 +19,7 @@ const {
19
unstable_scheduleCallback: Scheduler_scheduleCallback,
20
unstable_cancelCallback: Scheduler_cancelCallback,
21
unstable_shouldYield: Scheduler_shouldYield,
22
+ unstable_requestPaint: Scheduler_requestPaint,
23
unstable_now: Scheduler_now,
24
unstable_getCurrentPriorityLevel: Scheduler_getCurrentPriorityLevel,
25
unstable_ImmediatePriority: Scheduler_ImmediatePriority,
@@ -63,6 +64,9 @@ export const IdlePriority: ReactPriorityLevel = 95;
64
export const NoPriority: ReactPriorityLevel = 90;
65
66
export const shouldYield = Scheduler_shouldYield;
67
+export const requestPaint =
68
+ // Fall back gracefully if we're running an older verison of Scheduler.
69
+ Scheduler_requestPaint !== undefined ? Scheduler_requestPaint : () => {};
70
71
let syncQueue: Array<SchedulerCallback> | null = null;
72
let immediateQueueCallbackNode: mixed | null = null;
packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.internal.js
+26
@@ -166,6 +166,32 @@ describe('ReactSchedulerIntegration', () => {
166
expect(Scheduler).toFlushAndYield(['A [UserBlocking]', 'B [Normal]']);
167
});
168
169
+ it('requests a paint after committing', () => {
170
+ const scheduleCallback = Scheduler.unstable_scheduleCallback;
171
+
172
+ const root = ReactNoop.createRoot();
173
+ root.render('Initial');
174
+ Scheduler.flushAll();
175
+
176
+ scheduleCallback(NormalPriority, () => Scheduler.yieldValue('A'));
177
+ scheduleCallback(NormalPriority, () => Scheduler.yieldValue('B'));
178
+ scheduleCallback(NormalPriority, () => Scheduler.yieldValue('C'));
179
+
180
+ // Schedule a React render. React will request a paint after committing it.
181
+ root.render('Update');
182
+
183
+ // Advance time just to be sure the next tasks have lower priority
184
+ Scheduler.advanceTime(2000);
185
+
186
+ scheduleCallback(NormalPriority, () => Scheduler.yieldValue('D'));
187
+ scheduleCallback(NormalPriority, () => Scheduler.yieldValue('E'));
188
+
189
+ // Flush everything up to the next paint. Should yield after the
190
+ // React commit.
191
+ Scheduler.unstable_flushUntilNextPaint();
192
+ expect(Scheduler).toHaveYielded(['A', 'B', 'C']);
193
+ });
194
+
195
// TODO
196
it.skip('passive effects have render priority even if they are flushed early', () => {});
197
});
packages/scheduler/npm/umd/scheduler.development.js
+8
@@ -47,6 +47,13 @@
47
);
48
}
49
50
+ function unstable_requestPaint() {
51
+ return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_requestPaint.apply(
52
+ this,
53
+ arguments
54
+ );
55
+ }
56
+
57
function unstable_runWithPriority() {
58
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
59
this,
@@ -108,6 +115,7 @@
115
unstable_scheduleCallback: unstable_scheduleCallback,
116
unstable_cancelCallback: unstable_cancelCallback,
117
unstable_shouldYield: unstable_shouldYield,
118
+ unstable_requestPaint: unstable_requestPaint,
119
unstable_runWithPriority: unstable_runWithPriority,
120
unstable_next: unstable_next,
121
unstable_wrapCallback: unstable_wrapCallback,
packages/scheduler/npm/umd/scheduler.production.min.js
+8
@@ -47,6 +47,13 @@
47
);
48
}
49
50
+ function unstable_requestPaint() {
51
+ return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_requestPaint.apply(
52
+ this,
53
+ arguments
54
+ );
55
+ }
56
+
57
function unstable_runWithPriority() {
58
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
59
this,
@@ -102,6 +109,7 @@
109
unstable_scheduleCallback: unstable_scheduleCallback,
110
unstable_cancelCallback: unstable_cancelCallback,
111
unstable_shouldYield: unstable_shouldYield,
112
+ unstable_requestPaint: unstable_requestPaint,
113
unstable_runWithPriority: unstable_runWithPriority,
114
unstable_next: unstable_next,
115
unstable_wrapCallback: unstable_wrapCallback,
packages/scheduler/npm/umd/scheduler.profiling.min.js
+8
@@ -47,6 +47,13 @@
47
);
48
}
49
50
+ function unstable_requestPaint() {
51
+ return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_requestPaint.apply(
52
+ this,
53
+ arguments
54
+ );
55
+ }
56
+
57
function unstable_runWithPriority() {
58
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
59
this,
@@ -102,6 +109,7 @@
109
unstable_scheduleCallback: unstable_scheduleCallback,
110
unstable_cancelCallback: unstable_cancelCallback,
111
unstable_shouldYield: unstable_shouldYield,
112
+ unstable_requestPaint: unstable_requestPaint,
113
unstable_runWithPriority: unstable_runWithPriority,
114
unstable_next: unstable_next,
115
unstable_wrapCallback: unstable_wrapCallback,
packages/scheduler/src/Scheduler.js
+4
@@ -16,6 +16,7 @@ import {
16
shouldYieldToHost,
17
getCurrentTime,
18
forceFrameRate,
19
+ requestPaint,
20
} from './SchedulerHostConfig';
21
22
// TODO: Use symbols?
@@ -506,6 +507,8 @@ function unstable_shouldYield() {
507
);
508
}
509
510
+const unstable_requestPaint = requestPaint;
511
+
512
export {
513
ImmediatePriority as unstable_ImmediatePriority,
514
UserBlockingPriority as unstable_UserBlockingPriority,
@@ -519,6 +522,7 @@ export {
522
unstable_wrapCallback,
523
unstable_getCurrentPriorityLevel,
524
unstable_shouldYield,
525
+ unstable_requestPaint,
526
unstable_continueExecution,
527
unstable_pauseExecution,
528
unstable_getFirstCallbackNode,
packages/scheduler/src/forks/SchedulerHostConfig.default.js
+17
-7
@@ -18,6 +18,7 @@ export let cancelHostCallback;
18
export let requestHostTimeout;
19
export let cancelHostTimeout;
20
export let shouldYieldToHost;
21
+export let requestPaint;
22
export let getCurrentTime;
23
export let forceFrameRate;
24
@@ -125,7 +126,7 @@ if (
126
shouldYieldToHost = function() {
127
return false;
128
};
128
- forceFrameRate = function() {};
129
+ requestPaint = forceFrameRate = function() {};
130
} else {
131
if (typeof console !== 'undefined') {
132
// TODO: Remove fb.me link
@@ -162,7 +163,8 @@ if (
163
164
// TODO: Make this configurable
165
// TODO: Adjust this based on priority?
165
- let maxFrameLength = 300;
166
+ let maxFrameLength = 150;
167
+ let needsPaint = false;
168
169
const isInputPending =
170
navigator !== undefined &&
@@ -181,11 +183,12 @@ if (
183
// main thread, so the browser can perform high priority tasks. The main
184
// ones are painting and user input. If we're certain there's no user
185
// input, then we can yield less often without making the app less
184
- // responsive. We'll eventually yield regardless, since there could be
185
- // other main thread tasks that we don't know about.
186
- if (isInputPending !== null && !isInputPending()) {
187
- // There's no pending input. Only yield if we've reached the max
188
- // frame length.
186
+ // responsive. We'll also check if a paint was requested. We'll eventually
187
+ // yield regardless, since there could be other main thread tasks that we
188
+ // don't know about.
189
+ if (!needsPaint && isInputPending !== null && !isInputPending()) {
190
+ // There's no pending input, and no task requested a paint. Only yield
191
+ // if we've reached the max frame length.
192
return currentTime >= frameDeadline + maxFrameLength;
193
}
194
// Either there is pending input, or there's no way for us to be sure
@@ -242,6 +245,9 @@ if (
245
port.postMessage(undefined);
246
throw error;
247
}
248
+ // Yielding to the browser will give it a chance to paint, so we can
249
+ // reset this.
250
+ needsPaint = false;
251
}
252
};
253
@@ -321,4 +327,8 @@ if (
327
localClearTimeout(timeoutID);
328
timeoutID = -1;
329
};
330
+
331
+ requestPaint = function() {
332
+ needsPaint = true;
333
+ };
334
}
packages/scheduler/src/forks/SchedulerHostConfig.mock.js
+36
-3
@@ -15,6 +15,8 @@ let yieldedValues: Array<mixed> | null = null;
15
let expectedNumberOfYields: number = -1;
16
let didStop: boolean = false;
17
let isFlushing: boolean = false;
18
+let needsPaint: boolean = false;
19
+let shouldYieldForPaint: boolean = false;
20
21
export function requestHostCallback(callback: boolean => void) {
22
scheduledCallback = callback;
@@ -36,9 +38,10 @@ export function cancelHostTimeout(): void {
38
39
export function shouldYieldToHost(): boolean {
40
if (
39
- expectedNumberOfYields !== -1 &&
40
- yieldedValues !== null &&
41
- yieldedValues.length >= expectedNumberOfYields
41
+ (expectedNumberOfYields !== -1 &&
42
+ yieldedValues !== null &&
43
+ yieldedValues.length >= expectedNumberOfYields) ||
44
+ (shouldYieldForPaint && needsPaint)
45
) {
46
// We yielded at least as many values as expected. Stop flushing.
47
didStop = true;
@@ -67,6 +70,7 @@ export function reset() {
70
expectedNumberOfYields = -1;
71
didStop = false;
72
isFlushing = false;
73
+ needsPaint = false;
74
}
75
76
// Should only be used via an assertion helper that inspects the yielded values.
@@ -94,6 +98,31 @@ export function unstable_flushNumberOfYields(count: number): void {
98
}
99
}
100
101
+export function unstable_flushUntilNextPaint(): void {
102
+ if (isFlushing) {
103
+ throw new Error('Already flushing work.');
104
+ }
105
+ if (scheduledCallback !== null) {
106
+ const cb = scheduledCallback;
107
+ shouldYieldForPaint = true;
108
+ needsPaint = false;
109
+ isFlushing = true;
110
+ try {
111
+ let hasMoreWork = true;
112
+ do {
113
+ hasMoreWork = cb(true, currentTime);
114
+ } while (hasMoreWork && !didStop);
115
+ if (!hasMoreWork) {
116
+ scheduledCallback = null;
117
+ }
118
+ } finally {
119
+ shouldYieldForPaint = false;
120
+ didStop = false;
121
+ isFlushing = false;
122
+ }
123
+ }
124
+}
125
+
126
export function unstable_flushExpired() {
127
if (isFlushing) {
128
throw new Error('Already flushing work.');
@@ -181,3 +210,7 @@ export function advanceTime(ms: number) {
210
unstable_flushExpired();
211
}
212
}
213
+
214
+export function requestPaint() {
215
+ needsPaint = true;
216
+}
packages/scheduler/unstable_mock.js
+1
@@ -14,6 +14,7 @@ export {
14
unstable_flushNumberOfYields,
15
unstable_flushExpired,
16
unstable_clearYields,
17
+ unstable_flushUntilNextPaint,
18
flushAll,
19
yieldValue,
20
advanceTime,
packages/shared/forks/Scheduler.umd.js
+2
@@ -16,6 +16,7 @@ const {
16
unstable_now,
17
unstable_scheduleCallback,
18
unstable_shouldYield,
19
+ unstable_requestPaint,
20
unstable_getFirstCallbackNode,
21
unstable_runWithPriority,
22
unstable_next,
@@ -39,6 +40,7 @@ export {
40
unstable_now,
41
unstable_scheduleCallback,
42
unstable_shouldYield,
43
+ unstable_requestPaint,
44
unstable_getFirstCallbackNode,
45
unstable_runWithPriority,
46
unstable_next,