@samitouri / QOS-React-2 / commits / 518ce9c25f

Add Lazy Elements Behind a Flag (#19033)

We really needed this for Flight before as well but we got away with it because Blocks were lazy but with the removal of Blocks, we'll need this to ensure that we can lazily stream in part of the content. Luckily LazyComponent isn't really just a Component. It's just a generic type that can resolve into anything kind of like a Promise. So we can use that to resolve elements just like we can components. This allows keys and props to become lazy as well. To accomplish this, we suspend during reconciliation. This causes us to not be able to render siblings because we don't know if the keys will reconcile. For initial render we could probably special case this and just render a lazy component fiber. Throwing in reconciliation didn't work correctly with direct nested siblings of a Suspense boundary before but it does now so it depends on new reconciler.

Sebastian Markbåge committed May 28, 2020 at 14:16 UTC 518ce9c25f18075214cb572e7dc240598090e648
11 files changed +193 -2
packages/react-reconciler/src/ReactChildFiber.new.js
+52 -1
@@ -33,7 +33,11 @@ import {
33 Block,
34 } from './ReactWorkTags';
35 import invariant from 'shared/invariant';
36 -import {warnAboutStringRefs, enableBlocksAPI} from 'shared/ReactFeatureFlags';
36 +import {
37 + warnAboutStringRefs,
38 + enableBlocksAPI,
39 + enableLazyElements,
40 +} from 'shared/ReactFeatureFlags';
41
42 import {
43 createWorkInProgress,
@@ -532,6 +536,13 @@ function ChildReconciler(shouldTrackSideEffects) {
536 created.return = returnFiber;
537 return created;
538 }
539 + case REACT_LAZY_TYPE: {
540 + if (enableLazyElements) {
541 + const payload = newChild._payload;
542 + const init = newChild._init;
543 + return createChild(returnFiber, init(payload), lanes);
544 + }
545 + }
546 }
547
548 if (isArray(newChild) || getIteratorFn(newChild)) {
@@ -602,6 +613,13 @@ function ChildReconciler(shouldTrackSideEffects) {
613 return null;
614 }
615 }
616 + case REACT_LAZY_TYPE: {
617 + if (enableLazyElements) {
618 + const payload = newChild._payload;
619 + const init = newChild._init;
620 + return updateSlot(returnFiber, oldFiber, init(payload), lanes);
621 + }
622 + }
623 }
624
625 if (isArray(newChild) || getIteratorFn(newChild)) {
@@ -663,6 +681,18 @@ function ChildReconciler(shouldTrackSideEffects) {
681 ) || null;
682 return updatePortal(returnFiber, matchedFiber, newChild, lanes);
683 }
684 + case REACT_LAZY_TYPE:
685 + if (enableLazyElements) {
686 + const payload = newChild._payload;
687 + const init = newChild._init;
688 + return updateFromMap(
689 + existingChildren,
690 + returnFiber,
691 + newIdx,
692 + init(payload),
693 + lanes,
694 + );
695 + }
696 }
697
698 if (isArray(newChild) || getIteratorFn(newChild)) {
@@ -720,6 +750,15 @@ function ChildReconciler(shouldTrackSideEffects) {
750 key,
751 );
752 break;
753 + case REACT_LAZY_TYPE:
754 + if (enableLazyElements) {
755 + const payload = child._payload;
756 + const init = (child._init: any);
757 + warnOnInvalidKey(init(payload), knownKeys, returnFiber);
758 + break;
759 + }
760 + // We intentionally fallthrough here if enableLazyElements is not on.
761 + // eslint-disable-next-lined no-fallthrough
762 default:
763 break;
764 }
@@ -1276,6 +1315,18 @@ function ChildReconciler(shouldTrackSideEffects) {
1315 lanes,
1316 ),
1317 );
1318 + case REACT_LAZY_TYPE:
1319 + if (enableLazyElements) {
1320 + const payload = newChild._payload;
1321 + const init = newChild._init;
1322 + // TODO: This function is supposed to be non-recursive.
1323 + return reconcileChildFibers(
1324 + returnFiber,
1325 + currentFirstChild,
1326 + init(payload),
1327 + lanes,
1328 + );
1329 + }
1330 }
1331 }
1332
packages/react-reconciler/src/ReactChildFiber.old.js
+57 -1
@@ -33,7 +33,11 @@ import {
33 Block,
34 } from './ReactWorkTags';
35 import invariant from 'shared/invariant';
36 -import {warnAboutStringRefs, enableBlocksAPI} from 'shared/ReactFeatureFlags';
36 +import {
37 + warnAboutStringRefs,
38 + enableBlocksAPI,
39 + enableLazyElements,
40 +} from 'shared/ReactFeatureFlags';
41
42 import {
43 createWorkInProgress,
@@ -542,6 +546,13 @@ function ChildReconciler(shouldTrackSideEffects) {
546 created.return = returnFiber;
547 return created;
548 }
549 + case REACT_LAZY_TYPE: {
550 + if (enableLazyElements) {
551 + const payload = newChild._payload;
552 + const init = newChild._init;
553 + return createChild(returnFiber, init(payload), expirationTime);
554 + }
555 + }
556 }
557
558 if (isArray(newChild) || getIteratorFn(newChild)) {
@@ -627,6 +638,18 @@ function ChildReconciler(shouldTrackSideEffects) {
638 return null;
639 }
640 }
641 + case REACT_LAZY_TYPE: {
642 + if (enableLazyElements) {
643 + const payload = newChild._payload;
644 + const init = newChild._init;
645 + return updateSlot(
646 + returnFiber,
647 + oldFiber,
648 + init(payload),
649 + expirationTime,
650 + );
651 + }
652 + }
653 }
654
655 if (isArray(newChild) || getIteratorFn(newChild)) {
@@ -709,6 +732,18 @@ function ChildReconciler(shouldTrackSideEffects) {
732 expirationTime,
733 );
734 }
735 + case REACT_LAZY_TYPE:
736 + if (enableLazyElements) {
737 + const payload = newChild._payload;
738 + const init = newChild._init;
739 + return updateFromMap(
740 + existingChildren,
741 + returnFiber,
742 + newIdx,
743 + init(payload),
744 + expirationTime,
745 + );
746 + }
747 }
748
749 if (isArray(newChild) || getIteratorFn(newChild)) {
@@ -772,6 +807,15 @@ function ChildReconciler(shouldTrackSideEffects) {
807 key,
808 );
809 break;
810 + case REACT_LAZY_TYPE:
811 + if (enableLazyElements) {
812 + const payload = child._payload;
813 + const init = (child._init: any);
814 + warnOnInvalidKey(init(payload), knownKeys, returnFiber);
815 + break;
816 + }
817 + // We intentionally fallthrough here if enableLazyElements is not on.
818 + // eslint-disable-next-lined no-fallthrough
819 default:
820 break;
821 }
@@ -1349,6 +1393,18 @@ function ChildReconciler(shouldTrackSideEffects) {
1393 expirationTime,
1394 ),
1395 );
1396 + case REACT_LAZY_TYPE:
1397 + if (enableLazyElements) {
1398 + const payload = newChild._payload;
1399 + const init = newChild._init;
1400 + // TODO: This function is supposed to be non-recursive.
1401 + return reconcileChildFibers(
1402 + returnFiber,
1403 + currentFirstChild,
1404 + init(payload),
1405 + expirationTime,
1406 + );
1407 + }
1408 }
1409 }
1410
packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
+76
@@ -1273,4 +1273,80 @@ describe('ReactLazy', () => {
1273
1274 expect(componentStackMessage).toContain('in Lazy');
1275 });
1276 +
1277 + // @gate enableLazyElements && enableNewReconciler
1278 + it('mount and reorder lazy elements', async () => {
1279 + class Child extends React.Component {
1280 + componentDidMount() {
1281 + Scheduler.unstable_yieldValue('Did mount: ' + this.props.label);
1282 + }
1283 + componentDidUpdate() {
1284 + Scheduler.unstable_yieldValue('Did update: ' + this.props.label);
1285 + }
1286 + render() {
1287 + return <Text text={this.props.label} />;
1288 + }
1289 + }
1290 +
1291 + const lazyChildA = lazy(() => {
1292 + Scheduler.unstable_yieldValue('Init A');
1293 + return fakeImport(<Child key="A" label="A" />);
1294 + });
1295 + const lazyChildB = lazy(() => {
1296 + Scheduler.unstable_yieldValue('Init B');
1297 + return fakeImport(<Child key="B" label="B" />);
1298 + });
1299 + const lazyChildA2 = lazy(() => {
1300 + Scheduler.unstable_yieldValue('Init A2');
1301 + return fakeImport(<Child key="A" label="a" />);
1302 + });
1303 + const lazyChildB2 = lazy(() => {
1304 + Scheduler.unstable_yieldValue('Init B2');
1305 + return fakeImport(<Child key="B" label="b" />);
1306 + });
1307 +
1308 + function Parent({swap}) {
1309 + return (
1310 + <Suspense fallback={<Text text="Loading..." />}>
1311 + {swap ? [lazyChildB2, lazyChildA2] : [lazyChildA, lazyChildB]}
1312 + </Suspense>
1313 + );
1314 + }
1315 +
1316 + const root = ReactTestRenderer.create(<Parent swap={false} />, {
1317 + unstable_isConcurrent: true,
1318 + });
1319 +
1320 + expect(Scheduler).toFlushAndYield(['Init A', 'Loading...']);
1321 + expect(root).not.toMatchRenderedOutput('AB');
1322 +
1323 + await lazyChildA;
1324 + // We need to flush to trigger the B to load.
1325 + expect(Scheduler).toFlushAndYield(['Init B']);
1326 + await lazyChildB;
1327 +
1328 + expect(Scheduler).toFlushAndYield([
1329 + 'A',
1330 + 'B',
1331 + 'Did mount: A',
1332 + 'Did mount: B',
1333 + ]);
1334 + expect(root).toMatchRenderedOutput('AB');
1335 +
1336 + // Swap the position of A and B
1337 + root.update(<Parent swap={true} />);
1338 + expect(Scheduler).toFlushAndYield(['Init B2', 'Loading...']);
1339 + await lazyChildB2;
1340 + // We need to flush to trigger the second one to load.
1341 + expect(Scheduler).toFlushAndYield(['Init A2', 'Loading...']);
1342 + await lazyChildA2;
1343 +
1344 + expect(Scheduler).toFlushAndYield([
1345 + 'b',
1346 + 'a',
1347 + 'Did update: b',
1348 + 'Did update: a',
1349 + ]);
1350 + expect(root).toMatchRenderedOutput('ba');
1351 + });
1352 });
packages/shared/ReactFeatureFlags.js
+1
@@ -41,6 +41,7 @@ export const enableSelectiveHydration = __EXPERIMENTAL__;
41
42 // Flight experiments
43 export const enableBlocksAPI = __EXPERIMENTAL__;
44 +export const enableLazyElements = __EXPERIMENTAL__;
45
46 // Only used in www builds.
47 export const enableSchedulerDebugging = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -18,6 +18,7 @@ export const enableSchedulerTracing = __PROFILE__;
18 export const enableSuspenseServerRenderer = false;
19 export const enableSelectiveHydration = false;
20 export const enableBlocksAPI = false;
21 +export const enableLazyElements = false;
22 export const enableSchedulerDebugging = false;
23 export const debugRenderPhaseSideEffectsForStrictMode = true;
24 export const disableJavaScriptURLs = false;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -20,6 +20,7 @@ export const enableSchedulerTracing = __PROFILE__;
20 export const enableSuspenseServerRenderer = false;
21 export const enableSelectiveHydration = false;
22 export const enableBlocksAPI = false;
23 +export const enableLazyElements = false;
24 export const disableJavaScriptURLs = false;
25 export const disableInputAttributeSyncing = false;
26 export const enableSchedulerDebugging = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -20,6 +20,7 @@ export const enableSchedulerTracing = __PROFILE__;
20 export const enableSuspenseServerRenderer = false;
21 export const enableSelectiveHydration = false;
22 export const enableBlocksAPI = false;
23 +export const enableLazyElements = false;
24 export const disableJavaScriptURLs = false;
25 export const disableInputAttributeSyncing = false;
26 export const enableSchedulerDebugging = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -20,6 +20,7 @@ export const enableSchedulerTracing = __PROFILE__;
20 export const enableSuspenseServerRenderer = false;
21 export const enableSelectiveHydration = false;
22 export const enableBlocksAPI = false;
23 +export const enableLazyElements = false;
24 export const enableSchedulerDebugging = false;
25 export const disableJavaScriptURLs = false;
26 export const disableInputAttributeSyncing = false;
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -20,6 +20,7 @@ export const enableSchedulerTracing = __PROFILE__;
20 export const enableSuspenseServerRenderer = false;
21 export const enableSelectiveHydration = false;
22 export const enableBlocksAPI = false;
23 +export const enableLazyElements = false;
24 export const disableJavaScriptURLs = false;
25 export const disableInputAttributeSyncing = false;
26 export const enableSchedulerDebugging = false;
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -20,6 +20,7 @@ export const enableSchedulerTracing = false;
20 export const enableSuspenseServerRenderer = true;
21 export const enableSelectiveHydration = true;
22 export const enableBlocksAPI = true;
23 +export const enableLazyElements = false;
24 export const disableJavaScriptURLs = true;
25 export const disableInputAttributeSyncing = false;
26 export const enableSchedulerDebugging = false;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -50,6 +50,7 @@ export const enableSuspenseServerRenderer = true;
50 export const enableSelectiveHydration = true;
51
52 export const enableBlocksAPI = true;
53 +export const enableLazyElements = true;
54
55 export const disableJavaScriptURLs = true;
56