@samitouri / QOS-React-2 / commits / fe31cc710e

[Selective Hydration] Increase priority for non-synchronous discrete events and retries (#16935)

* Increase retryTime for increased priority dehydrated boundaries * Increaese the priority to user blocking for every next discrete boundary

Sebastian Markbåge committed Sep 27, 2019 at 17:27 UTC fe31cc710ec0a768bbeb9f382ec3055a09c97149
4 files changed +235 -3
packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
+176
@@ -114,4 +114,180 @@ describe('ReactDOMServerSelectiveHydration', () => {
114
115 document.body.removeChild(container);
116 });
117 +
118 + it('hydrates at higher pri if sync did not work first time', async () => {
119 + let suspend = false;
120 + let resolve;
121 + let promise = new Promise(resolvePromise => (resolve = resolvePromise));
122 +
123 + function Child({text}) {
124 + if ((text === 'A' || text === 'D') && suspend) {
125 + throw promise;
126 + }
127 + Scheduler.unstable_yieldValue(text);
128 + return (
129 + <span
130 + onClick={e => {
131 + e.preventDefault();
132 + Scheduler.unstable_yieldValue('Clicked ' + text);
133 + }}>
134 + {text}
135 + </span>
136 + );
137 + }
138 +
139 + function App() {
140 + Scheduler.unstable_yieldValue('App');
141 + return (
142 + <div>
143 + <Suspense fallback="Loading...">
144 + <Child text="A" />
145 + </Suspense>
146 + <Suspense fallback="Loading...">
147 + <Child text="B" />
148 + </Suspense>
149 + <Suspense fallback="Loading...">
150 + <Child text="C" />
151 + </Suspense>
152 + <Suspense fallback="Loading...">
153 + <Child text="D" />
154 + </Suspense>
155 + </div>
156 + );
157 + }
158 +
159 + let finalHTML = ReactDOMServer.renderToString(<App />);
160 +
161 + expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
162 +
163 + let container = document.createElement('div');
164 + // We need this to be in the document since we'll dispatch events on it.
165 + document.body.appendChild(container);
166 +
167 + container.innerHTML = finalHTML;
168 +
169 + let spanD = container.getElementsByTagName('span')[3];
170 +
171 + suspend = true;
172 +
173 + // A and D will be suspended. We'll click on D which should take
174 + // priority, after we unsuspend.
175 + let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
176 + root.render(<App />);
177 +
178 + // Nothing has been hydrated so far.
179 + expect(Scheduler).toHaveYielded([]);
180 +
181 + // This click target cannot be hydrated yet because it's suspended.
182 + let result = dispatchClickEvent(spanD);
183 +
184 + expect(Scheduler).toHaveYielded(['App']);
185 +
186 + expect(result).toBe(true);
187 +
188 + // Continuing rendering will render B next.
189 + expect(Scheduler).toFlushAndYield(['B', 'C']);
190 +
191 + suspend = false;
192 + resolve();
193 + await promise;
194 +
195 + // After the click, we should prioritize D and the Click first,
196 + // and only after that render A and C.
197 + expect(Scheduler).toFlushAndYield(['D', 'Clicked D', 'A']);
198 +
199 + document.body.removeChild(container);
200 + });
201 +
202 + it('hydrates at higher pri for secondary discrete events', async () => {
203 + let suspend = false;
204 + let resolve;
205 + let promise = new Promise(resolvePromise => (resolve = resolvePromise));
206 +
207 + function Child({text}) {
208 + if ((text === 'A' || text === 'D') && suspend) {
209 + throw promise;
210 + }
211 + Scheduler.unstable_yieldValue(text);
212 + return (
213 + <span
214 + onClick={e => {
215 + e.preventDefault();
216 + Scheduler.unstable_yieldValue('Clicked ' + text);
217 + }}>
218 + {text}
219 + </span>
220 + );
221 + }
222 +
223 + function App() {
224 + Scheduler.unstable_yieldValue('App');
225 + return (
226 + <div>
227 + <Suspense fallback="Loading...">
228 + <Child text="A" />
229 + </Suspense>
230 + <Suspense fallback="Loading...">
231 + <Child text="B" />
232 + </Suspense>
233 + <Suspense fallback="Loading...">
234 + <Child text="C" />
235 + </Suspense>
236 + <Suspense fallback="Loading...">
237 + <Child text="D" />
238 + </Suspense>
239 + </div>
240 + );
241 + }
242 +
243 + let finalHTML = ReactDOMServer.renderToString(<App />);
244 +
245 + expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
246 +
247 + let container = document.createElement('div');
248 + // We need this to be in the document since we'll dispatch events on it.
249 + document.body.appendChild(container);
250 +
251 + container.innerHTML = finalHTML;
252 +
253 + let spanA = container.getElementsByTagName('span')[0];
254 + let spanC = container.getElementsByTagName('span')[2];
255 + let spanD = container.getElementsByTagName('span')[3];
256 +
257 + suspend = true;
258 +
259 + // A and D will be suspended. We'll click on D which should take
260 + // priority, after we unsuspend.
261 + let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
262 + root.render(<App />);
263 +
264 + // Nothing has been hydrated so far.
265 + expect(Scheduler).toHaveYielded([]);
266 +
267 + // This click target cannot be hydrated yet because the first is Suspended.
268 + dispatchClickEvent(spanA);
269 + dispatchClickEvent(spanC);
270 + dispatchClickEvent(spanD);
271 +
272 + expect(Scheduler).toHaveYielded(['App']);
273 +
274 + suspend = false;
275 + resolve();
276 + await promise;
277 +
278 + // We should prioritize hydrating A, C and D first since we clicked in
279 + // them. Only after they're done will we hydrate B.
280 + expect(Scheduler).toFlushAndYield([
281 + 'A',
282 + 'Clicked A',
283 + 'C',
284 + 'Clicked C',
285 + 'D',
286 + 'Clicked D',
287 + // B should render last since it wasn't clicked.
288 + 'B',
289 + ]);
290 +
291 + document.body.removeChild(container);
292 + });
293 });
packages/react-dom/src/client/ReactDOM.js
+6 -1
@@ -40,6 +40,7 @@ import {
40 flushPassiveEffects,
41 IsThisRendererActing,
42 attemptSynchronousHydration,
43 + attemptUserBlockingHydration,
44 } from 'react-reconciler/inline.dom';
45 import {createPortal as createPortalImpl} from 'shared/ReactPortal';
46 import {canUseDOM} from 'shared/ExecutionEnvironment';
@@ -75,7 +76,10 @@ import {
76 } from './ReactDOMComponentTree';
77 import {restoreControlledState} from './ReactDOMComponent';
78 import {dispatchEvent} from '../events/ReactDOMEventListener';
78 -import {setAttemptSynchronousHydration} from '../events/ReactDOMEventReplaying';
79 +import {
80 + setAttemptSynchronousHydration,
81 + setAttemptUserBlockingHydration,
82 +} from '../events/ReactDOMEventReplaying';
83 import {eagerlyTrapReplayableEvents} from '../events/ReactDOMEventReplaying';
84 import {
85 ELEMENT_NODE,
@@ -86,6 +90,7 @@ import {
90 import {ROOT_ATTRIBUTE_NAME} from '../shared/DOMProperty';
91
92 setAttemptSynchronousHydration(attemptSynchronousHydration);
93 +setAttemptUserBlockingHydration(attemptUserBlockingHydration);
94
95 const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
96
packages/react-dom/src/events/ReactDOMEventReplaying.js
+12
@@ -37,6 +37,12 @@ export function setAttemptSynchronousHydration(fn: (fiber: Object) => void) {
37 attemptSynchronousHydration = fn;
38 }
39
40 +let attemptUserBlockingHydration: (fiber: Object) => void;
41 +
42 +export function setAttemptUserBlockingHydration(fn: (fiber: Object) => void) {
43 + attemptUserBlockingHydration = fn;
44 +}
45 +
46 // TODO: Upgrade this definition once we're on a newer version of Flow that
47 // has this definition built-in.
48 type PointerEvent = Event & {
@@ -436,6 +442,12 @@ function replayUnblockedEvents() {
442 let nextDiscreteEvent = queuedDiscreteEvents[0];
443 if (nextDiscreteEvent.blockedOn !== null) {
444 // We're still blocked.
445 + // Increase the priority of this boundary to unblock
446 + // the next discrete event.
447 + let fiber = getInstanceFromNode(nextDiscreteEvent.blockedOn);
448 + if (fiber !== null) {
449 + attemptUserBlockingHydration(fiber);
450 + }
451 break;
452 }
453 let nextBlockedOn = attemptToDispatchEvent(
packages/react-reconciler/src/ReactFiberReconciler.js
+41 -2
@@ -20,7 +20,10 @@ import {FundamentalComponent} from 'shared/ReactWorkTags';
20 import type {ReactNodeList} from 'shared/ReactTypes';
21 import type {ExpirationTime} from './ReactFiberExpirationTime';
22 import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
23 -import type {SuspenseHydrationCallbacks} from './ReactFiberSuspenseComponent';
23 +import type {
24 + SuspenseHydrationCallbacks,
25 + SuspenseState,
26 +} from './ReactFiberSuspenseComponent';
27
28 import {
29 findCurrentHostFiber,
@@ -75,7 +78,7 @@ import {
78 current as ReactCurrentFiberCurrent,
79 } from './ReactCurrentFiber';
80 import {StrictMode} from './ReactTypeOfMode';
78 -import {Sync} from './ReactFiberExpirationTime';
81 +import {Sync, computeInteractiveExpiration} from './ReactFiberExpirationTime';
82 import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig';
83 import {
84 scheduleRefresh,
@@ -378,10 +381,46 @@ export function attemptSynchronousHydration(fiber: Fiber): void {
381 break;
382 case SuspenseComponent:
383 flushSync(() => scheduleWork(fiber, Sync));
384 + // If we're still blocked after this, we need to increase
385 + // the priority of any promises resolving within this
386 + // boundary so that they next attempt also has higher pri.
387 + let retryExpTime = computeInteractiveExpiration(requestCurrentTime());
388 + markRetryTimeIfNotHydrated(fiber, retryExpTime);
389 break;
390 }
391 }
392
393 +function markRetryTimeImpl(fiber: Fiber, retryTime: ExpirationTime) {
394 + let suspenseState: null | SuspenseState = fiber.memoizedState;
395 + if (suspenseState !== null && suspenseState.dehydrated !== null) {
396 + if (suspenseState.retryTime < retryTime) {
397 + suspenseState.retryTime = retryTime;
398 + }
399 + }
400 +}
401 +
402 +// Increases the priority of thennables when they resolve within this boundary.
403 +function markRetryTimeIfNotHydrated(fiber: Fiber, retryTime: ExpirationTime) {
404 + markRetryTimeImpl(fiber, retryTime);
405 + let alternate = fiber.alternate;
406 + if (alternate) {
407 + markRetryTimeImpl(alternate, retryTime);
408 + }
409 +}
410 +
411 +export function attemptUserBlockingHydration(fiber: Fiber): void {
412 + if (fiber.tag !== SuspenseComponent) {
413 + // We ignore HostRoots here because we can't increase
414 + // their priority and they should not suspend on I/O,
415 + // since you have to wrap anything that might suspend in
416 + // Suspense.
417 + return;
418 + }
419 + let expTime = computeInteractiveExpiration(requestCurrentTime());
420 + scheduleWork(fiber, expTime);
421 + markRetryTimeIfNotHydrated(fiber, expTime);
422 +}
423 +
424 export {findHostInstance};
425
426 export {findHostInstanceWithWarning};