@samitouri / QOS-React-2 / commits / 933c664ad6

SuspenseList Optimizations (#16005)

* Add a bunch of optimizations to SuspenseList We now are able to bail out of reconciliation and splitting out the tail during deep updates that hasn't changed the child props. This only works while the list wasn't suspended before. I also moved the second render of the "head" to the complete phase. This cleans it up a bit for the tail collapsing PR. For this second pass I also use a new technique of resetting the child Fibers for the second pass. This is effectively a fast path to avoid reconciling the children against props again. * Move to didSuspend from SuspenseListState to the effectTag The effectTag now tracks whether the previous commit was suspended. This frees up SuspenseListState to be render-phase only state. We use null to mean the default "independent" mode. * Rename to SuspenseListState to SuspenseListRenderState * Reuse SuspenseListRenderState across render passes * Add optimization to bail out of scanning children if they can't be suspended This optimized the deep update case or initial render without anything suspending. We have some information available to us that tell us if nothing has suspended in the past and nothing has suspended this render pass. This also fixes a bug where we didn't tag the previous render as having suspended boundaries if we didn't need to force a rerender. * rm printChildren oops

Sebastian Markbåge committed Jul 1, 2019 at 14:25 UTC 933c664ad6173adda1021ee1cc2d9233c3c13e22
7 files changed +386 -256
packages/react-reconciler/src/ReactChildFiber.js
+13
@@ -33,6 +33,7 @@ import warningWithoutStack from 'shared/warningWithoutStack';
33
34 import {
35 createWorkInProgress,
36 + resetWorkInProgress,
37 createFiberFromElement,
38 createFiberFromFragment,
39 createFiberFromText,
@@ -1386,3 +1387,15 @@ export function cloneChildFibers(
1387 }
1388 newChild.sibling = null;
1389 }
1390 +
1391 +// Reset a workInProgress child set to prepare it for a second pass.
1392 +export function resetChildFibers(
1393 + workInProgress: Fiber,
1394 + renderExpirationTime: ExpirationTime,
1395 +): void {
1396 + let child = workInProgress.child;
1397 + while (child !== null) {
1398 + resetWorkInProgress(child, renderExpirationTime);
1399 + child = child.sibling;
1400 + }
1401 +}
packages/react-reconciler/src/ReactFiber.js
+73
@@ -481,6 +481,79 @@ export function createWorkInProgress(
481 return workInProgress;
482 }
483
484 +// Used to reuse a Fiber for a second pass.
485 +export function resetWorkInProgress(
486 + workInProgress: Fiber,
487 + renderExpirationTime: ExpirationTime,
488 +) {
489 + // This resets the Fiber to what createFiber or createWorkInProgress would
490 + // have set the values to before during the first pass. Ideally this wouldn't
491 + // be necessary but unfortunately many code paths reads from the workInProgress
492 + // when they should be reading from current and writing to workInProgress.
493 +
494 + // We assume pendingProps, index, key, ref, return are still untouched to
495 + // avoid doing another reconciliation.
496 +
497 + // Reset the effect tag.
498 + workInProgress.effectTag = NoEffect;
499 +
500 + // The effect list is no longer valid.
501 + workInProgress.nextEffect = null;
502 + workInProgress.firstEffect = null;
503 + workInProgress.lastEffect = null;
504 +
505 + let current = workInProgress.alternate;
506 + if (current === null) {
507 + // Reset to createFiber's initial values.
508 + workInProgress.childExpirationTime = NoWork;
509 + workInProgress.expirationTime = renderExpirationTime;
510 +
511 + workInProgress.child = null;
512 + workInProgress.memoizedProps = null;
513 + workInProgress.memoizedState = null;
514 + workInProgress.updateQueue = null;
515 +
516 + workInProgress.dependencies = null;
517 +
518 + if (enableProfilerTimer) {
519 + // Note: We don't reset the actualTime counts. It's useful to accumulate
520 + // actual time across multiple render passes.
521 + workInProgress.selfBaseDuration = 0;
522 + workInProgress.treeBaseDuration = 0;
523 + }
524 + } else {
525 + // Reset to the cloned values that createWorkInProgress would've.
526 + workInProgress.childExpirationTime = current.childExpirationTime;
527 + workInProgress.expirationTime = current.expirationTime;
528 +
529 + workInProgress.child = current.child;
530 + workInProgress.memoizedProps = current.memoizedProps;
531 + workInProgress.memoizedState = current.memoizedState;
532 + workInProgress.updateQueue = current.updateQueue;
533 +
534 + // Clone the dependencies object. This is mutated during the render phase, so
535 + // it cannot be shared with the current fiber.
536 + const currentDependencies = current.dependencies;
537 + workInProgress.dependencies =
538 + currentDependencies === null
539 + ? null
540 + : {
541 + expirationTime: currentDependencies.expirationTime,
542 + firstContext: currentDependencies.firstContext,
543 + events: currentDependencies.events,
544 + };
545 +
546 + if (enableProfilerTimer) {
547 + // Note: We don't reset the actualTime counts. It's useful to accumulate
548 + // actual time across multiple render passes.
549 + workInProgress.selfBaseDuration = current.selfBaseDuration;
550 + workInProgress.treeBaseDuration = current.treeBaseDuration;
551 + }
552 + }
553 +
554 + return workInProgress;
555 +}
556 +
557 export function createHostRootFiber(tag: RootTag): Fiber {
558 let mode;
559 if (tag === ConcurrentRoot) {
packages/react-reconciler/src/ReactFiberBeginWork.js
+167 -164
@@ -13,7 +13,7 @@ import type {FiberRoot} from './ReactFiberRoot';
13 import type {ExpirationTime} from './ReactFiberExpirationTime';
14 import type {
15 SuspenseState,
16 - SuspenseListState,
16 + SuspenseListRenderState,
17 } from './ReactFiberSuspenseComponent';
18 import type {SuspenseContext} from './ReactFiberSuspenseContext';
19
@@ -2007,6 +2007,88 @@ function findLastContentRow(firstChild: null | Fiber): null | Fiber {
2007
2008 type SuspenseListRevealOrder = 'forwards' | 'backwards' | 'together' | void;
2009
2010 +function validateRevealOrder(revealOrder: SuspenseListRevealOrder) {
2011 + if (__DEV__) {
2012 + if (
2013 + revealOrder !== undefined &&
2014 + revealOrder !== 'forwards' &&
2015 + revealOrder !== 'backwards' &&
2016 + revealOrder !== 'together' &&
2017 + !didWarnAboutRevealOrder[revealOrder]
2018 + ) {
2019 + didWarnAboutRevealOrder[revealOrder] = true;
2020 + if (typeof revealOrder === 'string') {
2021 + switch (revealOrder.toLowerCase()) {
2022 + case 'together':
2023 + case 'forwards':
2024 + case 'backwards': {
2025 + warning(
2026 + false,
2027 + '"%s" is not a valid value for revealOrder on <SuspenseList />. ' +
2028 + 'Use lowercase "%s" instead.',
2029 + revealOrder,
2030 + revealOrder.toLowerCase(),
2031 + );
2032 + break;
2033 + }
2034 + case 'forward':
2035 + case 'backward': {
2036 + warning(
2037 + false,
2038 + '"%s" is not a valid value for revealOrder on <SuspenseList />. ' +
2039 + 'React uses the -s suffix in the spelling. Use "%ss" instead.',
2040 + revealOrder,
2041 + revealOrder.toLowerCase(),
2042 + );
2043 + break;
2044 + }
2045 + default:
2046 + warning(
2047 + false,
2048 + '"%s" is not a supported revealOrder on <SuspenseList />. ' +
2049 + 'Did you mean "together", "forwards" or "backwards"?',
2050 + revealOrder,
2051 + );
2052 + break;
2053 + }
2054 + } else {
2055 + warning(
2056 + false,
2057 + '%s is not a supported value for revealOrder on <SuspenseList />. ' +
2058 + 'Did you mean "together", "forwards" or "backwards"?',
2059 + revealOrder,
2060 + );
2061 + }
2062 + }
2063 + }
2064 +}
2065 +
2066 +function initSuspenseListRenderState(
2067 + workInProgress: Fiber,
2068 + isBackwards: boolean,
2069 + tail: null | Fiber,
2070 + lastContentRow: null | Fiber,
2071 +): void {
2072 + let renderState: null | SuspenseListRenderState =
2073 + workInProgress.memoizedState;
2074 + if (renderState === null) {
2075 + workInProgress.memoizedState = {
2076 + isBackwards: isBackwards,
2077 + rendering: null,
2078 + last: lastContentRow,
2079 + tail: tail,
2080 + tailExpiration: 0,
2081 + };
2082 + } else {
2083 + // We can reuse the existing object from previous renders.
2084 + renderState.isBackwards = isBackwards;
2085 + renderState.rendering = null;
2086 + renderState.last = lastContentRow;
2087 + renderState.tail = tail;
2088 + renderState.tailExpiration = 0;
2089 + }
2090 +}
2091 +
2092 // This can end up rendering this component multiple passes.
2093 // The first pass splits the children fibers into two sets. A head and tail.
2094 // We first render the head. If anything is in fallback state, we do another
@@ -2021,24 +2103,11 @@ function updateSuspenseListComponent(
2103 ) {
2104 const nextProps = workInProgress.pendingProps;
2105 const revealOrder: SuspenseListRevealOrder = nextProps.revealOrder;
2024 - const nextChildren = nextProps.children;
2106 + const newChildren = nextProps.children;
2107
2026 - let nextChildFibers;
2027 - if (current === null) {
2028 - nextChildFibers = mountChildFibers(
2029 - workInProgress,
2030 - null,
2031 - nextChildren,
2032 - renderExpirationTime,
2033 - );
2034 - } else {
2035 - nextChildFibers = reconcileChildFibers(
2036 - workInProgress,
2037 - current.child,
2038 - nextChildren,
2039 - renderExpirationTime,
2040 - );
2041 - }
2108 + validateRevealOrder(revealOrder);
2109 +
2110 + reconcileChildren(current, workInProgress, newChildren, renderExpirationTime);
2111
2112 let suspenseContext: SuspenseContext = suspenseStackCursor.current;
2113
@@ -2046,108 +2115,71 @@ function updateSuspenseListComponent(
2115 suspenseContext,
2116 (ForceSuspenseFallback: SuspenseContext),
2117 );
2049 -
2050 - if ((workInProgress.effectTag & DidCapture) !== NoEffect) {
2051 - // This is the second pass. In this pass, we should force the
2052 - // fallbacks in place.
2053 - shouldForceFallback = true;
2054 - }
2055 -
2056 - let suspenseListState: null | SuspenseListState = null;
2057 -
2118 if (shouldForceFallback) {
2119 suspenseContext = setShallowSuspenseContext(
2120 suspenseContext,
2121 ForceSuspenseFallback,
2122 );
2063 - suspenseListState = {
2064 - didSuspend: true,
2065 - isBackwards: false,
2066 - rendering: null,
2067 - last: null,
2068 - tail: null,
2069 - tailExpiration: 0,
2070 - };
2123 + workInProgress.effectTag |= DidCapture;
2124 } else {
2072 - let didForceFallback =
2073 - current !== null &&
2074 - current.memoizedState !== null &&
2075 - (current.memoizedState: SuspenseListState).didSuspend;
2076 - if (didForceFallback) {
2125 + const didSuspendBefore =
2126 + current !== null && (current.effectTag & DidCapture) !== NoEffect;
2127 + if (didSuspendBefore) {
2128 // If we previously forced a fallback, we need to schedule work
2129 // on any nested boundaries to let them know to try to render
2130 // again. This is the same as context updating.
2131 propagateSuspenseContextChange(
2132 workInProgress,
2082 - nextChildFibers,
2133 + workInProgress.child,
2134 renderExpirationTime,
2135 );
2136 }
2137 suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
2138 }
2088 -
2139 pushSuspenseContext(workInProgress, suspenseContext);
2140
2141 if ((workInProgress.mode & BatchedMode) === NoMode) {
2142 + workInProgress.memoizedState = null;
2143 + } else {
2144 // Outside of batched mode, SuspenseList doesn't work so we just
2145 // use make it a noop by treating it as the default revealOrder.
2094 - workInProgress.effectTag |= DidCapture;
2095 - workInProgress.child = nextChildFibers;
2096 - return nextChildFibers;
2097 - }
2098 -
2099 - switch (revealOrder) {
2100 - case 'forwards': {
2101 - // If need to force fallbacks in this pass we're just going to
2102 - // force the whole set to suspend so we don't have to do anything
2103 - // further here.
2104 - if (!shouldForceFallback) {
2105 - let lastContentRow = findLastContentRow(nextChildFibers);
2146 + switch (revealOrder) {
2147 + case 'forwards': {
2148 + let lastContentRow = findLastContentRow(workInProgress.child);
2149 let tail;
2150 if (lastContentRow === null) {
2151 // The whole list is part of the tail.
2152 // TODO: We could fast path by just rendering the tail now.
2110 - tail = nextChildFibers;
2111 - nextChildFibers = null;
2153 + tail = workInProgress.child;
2154 + workInProgress.child = null;
2155 } else {
2156 // Disconnect the tail rows after the content row.
2157 // We're going to render them separately later.
2158 tail = lastContentRow.sibling;
2159 lastContentRow.sibling = null;
2160 }
2118 - if (suspenseListState === null) {
2119 - suspenseListState = {
2120 - didSuspend: false,
2121 - isBackwards: false,
2122 - rendering: null,
2123 - last: lastContentRow,
2124 - tail: tail,
2125 - tailExpiration: 0,
2126 - };
2127 - } else {
2128 - suspenseListState.tail = tail;
2129 - }
2161 + initSuspenseListRenderState(
2162 + workInProgress,
2163 + false, // isBackwards
2164 + tail,
2165 + lastContentRow,
2166 + );
2167 + break;
2168 }
2131 - break;
2132 - }
2133 - case 'backwards': {
2134 - // If need to force fallbacks in this pass we're just going to
2135 - // force the whole set to suspend so we don't have to do anything
2136 - // further here.
2137 - if (!shouldForceFallback) {
2169 + case 'backwards': {
2170 // We're going to find the first row that has existing content.
2171 // At the same time we're going to reverse the list of everything
2172 // we pass in the meantime. That's going to be our tail in reverse
2173 // order.
2174 let tail = null;
2143 - let row = nextChildFibers;
2144 - nextChildFibers = null;
2175 + let row = workInProgress.child;
2176 + workInProgress.child = null;
2177 while (row !== null) {
2178 let currentRow = row.alternate;
2179 // New rows can't be content rows.
2180 if (currentRow !== null && !isShowingAnyFallbacks(currentRow)) {
2181 // This is the beginning of the main content.
2150 - nextChildFibers = row;
2182 + workInProgress.child = row;
2183 break;
2184 }
2185 let nextRow = row.sibling;
@@ -2155,88 +2187,32 @@ function updateSuspenseListComponent(
2187 tail = row;
2188 row = nextRow;
2189 }
2158 - // TODO: If nextChildFibers is null, we can continue on the tail immediately.
2159 - if (suspenseListState === null) {
2160 - suspenseListState = {
2161 - didSuspend: false,
2162 - isBackwards: true,
2163 - rendering: null,
2164 - last: null,
2165 - tail: tail,
2166 - tailExpiration: 0,
2167 - };
2168 - } else {
2169 - suspenseListState.isBackwards = true;
2170 - suspenseListState.tail = tail;
2171 - }
2190 + // TODO: If workInProgress.child is null, we can continue on the tail immediately.
2191 + initSuspenseListRenderState(
2192 + workInProgress,
2193 + true, // isBackwards
2194 + tail,
2195 + null, // last
2196 + );
2197 + break;
2198 }
2173 - break;
2174 - }
2175 - case 'together': {
2176 - break;
2177 - }
2178 - default: {
2179 - // The default reveal order is the same as not having
2180 - // a boundary.
2181 - if (__DEV__) {
2182 - if (
2183 - revealOrder !== undefined &&
2184 - !didWarnAboutRevealOrder[revealOrder]
2185 - ) {
2186 - didWarnAboutRevealOrder[revealOrder] = true;
2187 - if (typeof revealOrder === 'string') {
2188 - switch (revealOrder.toLowerCase()) {
2189 - case 'together':
2190 - case 'forwards':
2191 - case 'backwards': {
2192 - warning(
2193 - false,
2194 - '"%s" is not a valid value for revealOrder on <SuspenseList />. ' +
2195 - 'Use lowercase "%s" instead.',
2196 - revealOrder,
2197 - revealOrder.toLowerCase(),
2198 - );
2199 - break;
2200 - }
2201 - case 'forward':
2202 - case 'backward': {
2203 - warning(
2204 - false,
2205 - '"%s" is not a valid value for revealOrder on <SuspenseList />. ' +
2206 - 'React uses the -s suffix in the spelling. Use "%ss" instead.',
2207 - revealOrder,
2208 - revealOrder.toLowerCase(),
2209 - );
2210 - break;
2211 - }
2212 - default:
2213 - warning(
2214 - false,
2215 - '"%s" is not a supported revealOrder on <SuspenseList />. ' +
2216 - 'Did you mean "together", "forwards" or "backwards"?',
2217 - revealOrder,
2218 - );
2219 - break;
2220 - }
2221 - } else {
2222 - warning(
2223 - false,
2224 - '%s is not a supported value for revealOrder on <SuspenseList />. ' +
2225 - 'Did you mean "together", "forwards" or "backwards"?',
2226 - revealOrder,
2227 - );
2228 - }
2229 - }
2199 + case 'together': {
2200 + initSuspenseListRenderState(
2201 + workInProgress,
2202 + false, // isBackwards
2203 + null, // tail
2204 + null, // last
2205 + );
2206 + break;
2207 + }
2208 + default: {
2209 + // The default reveal order is the same as not having
2210 + // a boundary.
2211 + workInProgress.memoizedState = null;
2212 }
2231 - // We mark this as having captured but it really just says to the
2232 - // complete phase that we should treat this as done, whatever form
2233 - // it is in. No need for a second pass.
2234 - workInProgress.effectTag |= DidCapture;
2213 }
2214 }
2237 - workInProgress.memoizedState = suspenseListState;
2238 - workInProgress.child = nextChildFibers;
2239 - return nextChildFibers;
2215 + return workInProgress.child;
2216 }
2217
2218 function updatePortalComponent(
@@ -2657,19 +2633,46 @@ function beginWork(
2633 break;
2634 }
2635 case SuspenseListComponent: {
2660 - // Check if the children have any pending work.
2636 + const didSuspendBefore =
2637 + (current.effectTag & DidCapture) !== NoEffect;
2638 +
2639 const childExpirationTime = workInProgress.childExpirationTime;
2640 if (childExpirationTime < renderExpirationTime) {
2641 + // If none of the children had any work, that means that none of
2642 + // them got retried so they'll still be blocked in the same way
2643 + // as before. We can fast bail out.
2644 pushSuspenseContext(workInProgress, suspenseStackCursor.current);
2664 - // None of the children have any work, so we can do a fast bailout.
2645 + if (didSuspendBefore) {
2646 + workInProgress.effectTag |= DidCapture;
2647 + }
2648 return null;
2649 }
2667 - // Try the normal path.
2668 - return updateSuspenseListComponent(
2669 - current,
2670 - workInProgress,
2671 - renderExpirationTime,
2672 - );
2650 +
2651 + if (didSuspendBefore) {
2652 + // If something was in fallback state last time, and we have all the
2653 + // same children then we're still in progressive loading state.
2654 + // Something might get unblocked by state updates or retries in the
2655 + // tree which will affect the tail. So we need to use the normal
2656 + // path to compute the correct tail.
2657 + return updateSuspenseListComponent(
2658 + current,
2659 + workInProgress,
2660 + renderExpirationTime,
2661 + );
2662 + }
2663 +
2664 + // If nothing suspended before and we're rendering the same children,
2665 + // then the tail doesn't matter. Anything new that suspends will work
2666 + // in the "together" mode, so we can continue from the state we had.
2667 + let renderState = workInProgress.memoizedState;
2668 + if (renderState !== null) {
2669 + // Reset to the "together" mode in case we've started a different
2670 + // update in the past but didn't complete it.
2671 + renderState.rendering = null;
2672 + renderState.tail = null;
2673 + }
2674 + pushSuspenseContext(workInProgress, suspenseStackCursor.current);
2675 + break;
2676 }
2677 case EventComponent:
2678 if (enableFlareAPI) {
packages/react-reconciler/src/ReactFiberCompleteWork.js
+120 -86
@@ -20,7 +20,7 @@ import type {
20 import type {ReactEventComponentInstance} from 'shared/ReactTypes';
21 import type {
22 SuspenseState,
23 - SuspenseListState,
23 + SuspenseListRenderState,
24 } from './ReactFiberSuspenseComponent';
25 import type {SuspenseContext} from './ReactFiberSuspenseContext';
26
@@ -114,6 +114,7 @@ import {
114 markSpawnedWork,
115 renderDidSuspend,
116 renderDidSuspendDelayIfPossible,
117 + renderHasNotSuspendedYet,
118 } from './ReactFiberWorkLoop';
119 import {
120 getEventComponentHostChildrenCount,
@@ -122,6 +123,7 @@ import {
123 import getComponentName from 'shared/getComponentName';
124 import warning from 'shared/warning';
125 import {Never} from './ReactFiberExpirationTime';
126 +import {resetChildFibers} from './ReactChildFiber';
127
128 function markUpdate(workInProgress: Fiber) {
129 // Tag the fiber with an update effect. This turns a Placement into
@@ -553,6 +555,11 @@ function hasSuspendedChildrenAndNewContent(
555 const state: SuspenseState | null = node.memoizedState;
556 const isShowingFallback = state !== null;
557 if (isShowingFallback) {
558 + // Tag the parent fiber as having suspended boundaries.
559 + if (!hasSuspendedBoundaries) {
560 + workInProgress.effectTag |= DidCapture;
561 + }
562 +
563 hasSuspendedBoundaries = true;
564
565 if (node.updateQueue !== null) {
@@ -928,112 +935,139 @@ function completeWork(
935 case SuspenseListComponent: {
936 popSuspenseContext(workInProgress);
937
931 - if ((workInProgress.effectTag & DidCapture) === NoEffect) {
932 - let suspenseListState: null | SuspenseListState =
933 - workInProgress.memoizedState;
934 - if (
935 - suspenseListState === null ||
936 - suspenseListState.rendering === null
937 - ) {
938 + const renderState: null | SuspenseListRenderState =
939 + workInProgress.memoizedState;
940 +
941 + if (renderState === null) {
942 + // We're running in the default, "independent" mode. We don't do anything
943 + // in this mode.
944 + break;
945 + }
946 +
947 + let didSuspendAlready =
948 + (workInProgress.effectTag & DidCapture) !== NoEffect;
949 +
950 + let renderedTail = renderState.rendering;
951 + if (renderedTail === null) {
952 + // We just rendered the head.
953 + if (!didSuspendAlready) {
954 // This is the first pass. We need to figure out if anything is still
955 // suspended in the rendered set.
956 const renderedChildren = workInProgress.child;
957 // If new content unsuspended, but there's still some content that
958 // didn't. Then we need to do a second pass that forces everything
959 // to keep showing their fallbacks.
944 - const needsRerender = hasSuspendedChildrenAndNewContent(
945 - workInProgress,
946 - renderedChildren,
947 - );
960 +
961 + // We might be suspended if something in this render pass suspended, or
962 + // something in the previous committed pass suspended. Otherwise,
963 + // there's no chance so we can skip the expensive call to
964 + // hasSuspendedChildrenAndNewContent.
965 + let cannotBeSuspended =
966 + renderHasNotSuspendedYet() &&
967 + (current === null || (current.effectTag & DidCapture) === NoEffect);
968 + let needsRerender =
969 + !cannotBeSuspended &&
970 + hasSuspendedChildrenAndNewContent(workInProgress, renderedChildren);
971 if (needsRerender) {
972 // Rerender the whole list, but this time, we'll force fallbacks
973 // to stay in place.
951 - workInProgress.effectTag |= DidCapture;
974 // Reset the effect list before doing the second pass since that's now invalid.
975 workInProgress.firstEffect = workInProgress.lastEffect = null;
954 - // Schedule work so we know not to bail out.
955 - workInProgress.expirationTime = renderExpirationTime;
956 - return workInProgress;
976 + // Reset the child fibers to their original state.
977 + resetChildFibers(workInProgress, renderExpirationTime);
978 +
979 + // Set up the Suspense Context to force suspense and immediately
980 + // rerender the children.
981 + pushSuspenseContext(
982 + workInProgress,
983 + setShallowSuspenseContext(
984 + suspenseStackCursor.current,
985 + ForceSuspenseFallback,
986 + ),
987 + );
988 + return workInProgress.child;
989 }
958 - } else {
959 - // Append the rendered row to the child list.
960 - let rendered = suspenseListState.rendering;
961 - if (!suspenseListState.didSuspend) {
962 - if (
963 - now() > suspenseListState.tailExpiration &&
964 - renderExpirationTime > Never
965 - ) {
966 - // We have now passed our CPU deadline and we'll just give up further
967 - // attempts to render the main content and only render fallbacks.
968 - // The assumption is that this is usually faster.
969 - suspenseListState.didSuspend = true;
970 - // Since nothing actually suspended, there will nothing to ping this
971 - // to get it started back up to attempt the next item. If we can show
972 - // them, then they really have the same priority as this render.
973 - // So we'll pick it back up the very next render pass once we've had
974 - // an opportunity to yield for paint.
990 + // hasSuspendedChildrenAndNewContent could've set didSuspendAlready
991 + didSuspendAlready =
992 + (workInProgress.effectTag & DidCapture) !== NoEffect;
993 + }
994 + // Next we're going to render the tail.
995 + } else {
996 + // Append the rendered row to the child list.
997 + if (!didSuspendAlready) {
998 + if (
999 + now() > renderState.tailExpiration &&
1000 + renderExpirationTime > Never
1001 + ) {
1002 + // We have now passed our CPU deadline and we'll just give up further
1003 + // attempts to render the main content and only render fallbacks.
1004 + // The assumption is that this is usually faster.
1005 + workInProgress.effectTag |= DidCapture;
1006 + didSuspendAlready = true;
1007 + // Since nothing actually suspended, there will nothing to ping this
1008 + // to get it started back up to attempt the next item. If we can show
1009 + // them, then they really have the same priority as this render.
1010 + // So we'll pick it back up the very next render pass once we've had
1011 + // an opportunity to yield for paint.
1012
976 - const nextPriority = renderExpirationTime - 1;
977 - workInProgress.expirationTime = workInProgress.childExpirationTime = nextPriority;
978 - if (enableSchedulerTracing) {
979 - markSpawnedWork(nextPriority);
980 - }
981 - } else {
982 - suspenseListState.didSuspend = isShowingAnyFallbacks(rendered);
1013 + const nextPriority = renderExpirationTime - 1;
1014 + workInProgress.expirationTime = workInProgress.childExpirationTime = nextPriority;
1015 + if (enableSchedulerTracing) {
1016 + markSpawnedWork(nextPriority);
1017 }
1018 + } else if (isShowingAnyFallbacks(renderedTail)) {
1019 + workInProgress.effectTag |= DidCapture;
1020 + didSuspendAlready = true;
1021 }
985 - if (suspenseListState.isBackwards) {
986 - // The effect list of the backwards tail will have been added
987 - // to the end. This breaks the guarantee that life-cycles fire in
988 - // sibling order but that isn't a strong guarantee promised by React.
989 - // Especially since these might also just pop in during future commits.
990 - // Append to the beginning of the list.
991 - rendered.sibling = workInProgress.child;
992 - workInProgress.child = rendered;
1022 + }
1023 + if (renderState.isBackwards) {
1024 + // The effect list of the backwards tail will have been added
1025 + // to the end. This breaks the guarantee that life-cycles fire in
1026 + // sibling order but that isn't a strong guarantee promised by React.
1027 + // Especially since these might also just pop in during future commits.
1028 + // Append to the beginning of the list.
1029 + renderedTail.sibling = workInProgress.child;
1030 + workInProgress.child = renderedTail;
1031 + } else {
1032 + let previousSibling = renderState.last;
1033 + if (previousSibling !== null) {
1034 + previousSibling.sibling = renderedTail;
1035 } else {
994 - let previousSibling = suspenseListState.last;
995 - if (previousSibling !== null) {
996 - previousSibling.sibling = rendered;
997 - } else {
998 - workInProgress.child = rendered;
999 - }
1000 - suspenseListState.last = rendered;
1036 + workInProgress.child = renderedTail;
1037 }
1038 + renderState.last = renderedTail;
1039 }
1040 + }
1041
1004 - if (suspenseListState !== null && suspenseListState.tail !== null) {
1005 - // We still have tail rows to render.
1006 - if (suspenseListState.tailExpiration === 0) {
1007 - // Heuristic for how long we're willing to spend rendering rows
1008 - // until we just give up and show what we have so far.
1009 - const TAIL_EXPIRATION_TIMEOUT_MS = 500;
1010 - suspenseListState.tailExpiration =
1011 - now() + TAIL_EXPIRATION_TIMEOUT_MS;
1012 - }
1013 - // Pop a row.
1014 - let next = suspenseListState.tail;
1015 - suspenseListState.rendering = next;
1016 - suspenseListState.tail = next.sibling;
1017 - next.sibling = null;
1042 + if (renderState.tail !== null) {
1043 + // We still have tail rows to render.
1044 + if (renderState.tailExpiration === 0) {
1045 + // Heuristic for how long we're willing to spend rendering rows
1046 + // until we just give up and show what we have so far.
1047 + const TAIL_EXPIRATION_TIMEOUT_MS = 500;
1048 + renderState.tailExpiration = now() + TAIL_EXPIRATION_TIMEOUT_MS;
1049 + }
1050 + // Pop a row.
1051 + let next = renderState.tail;
1052 + renderState.rendering = next;
1053 + renderState.tail = next.sibling;
1054 + next.sibling = null;
1055
1019 - // Restore the context.
1020 - // TODO: We can probably just avoid popping it instead and only
1021 - // setting it the first time we go from not suspended to suspended.
1022 - let suspenseContext = suspenseStackCursor.current;
1023 - if (suspenseListState.didSuspend) {
1024 - suspenseContext = setShallowSuspenseContext(
1025 - suspenseContext,
1026 - ForceSuspenseFallback,
1027 - );
1028 - } else {
1029 - suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
1030 - }
1031 - pushSuspenseContext(workInProgress, suspenseContext);
1032 - // Do a pass over the next row.
1033 - return next;
1056 + // Restore the context.
1057 + // TODO: We can probably just avoid popping it instead and only
1058 + // setting it the first time we go from not suspended to suspended.
1059 + let suspenseContext = suspenseStackCursor.current;
1060 + if (didSuspendAlready) {
1061 + suspenseContext = setShallowSuspenseContext(
1062 + suspenseContext,
1063 + ForceSuspenseFallback,
1064 + );
1065 + } else {
1066 + suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
1067 }
1035 - } else {
1036 - workInProgress.effectTag &= ~DidCapture;
1068 + pushSuspenseContext(workInProgress, suspenseContext);
1069 + // Do a pass over the next row.
1070 + return next;
1071 }
1072 break;
1073 }
packages/react-reconciler/src/ReactFiberSuspenseComponent.js
+2 -2
@@ -11,10 +11,10 @@ import type {Fiber} from './ReactFiber';
11 import {SuspenseComponent} from 'shared/ReactWorkTags';
12
13 // TODO: This is now an empty object. Should we switch this to a boolean?
14 +// Alternatively we can make this use an effect tag similar to SuspenseList.
15 export type SuspenseState = {||};
16
16 -export type SuspenseListState = {|
17 - didSuspend: boolean,
17 +export type SuspenseListRenderState = {|
18 isBackwards: boolean,
19 // The currently rendering tail row.
20 rendering: null | Fiber,
packages/react-reconciler/src/ReactFiberWorkLoop.js
+8
@@ -1201,6 +1201,14 @@ export function renderDidError() {
1201 }
1202 }
1203
1204 +// Called during render to determine if anything has suspended.
1205 +// Returns false if we're not sure.
1206 +export function renderHasNotSuspendedYet(): boolean {
1207 + // If something errored or completed, we can't really be sure,
1208 + // so those are false.
1209 + return workInProgressRootExitStatus === RootIncomplete;
1210 +}
1211 +
1212 function inferTimeFromExpirationTime(expirationTime: ExpirationTime): number {
1213 // We don't know exactly when the update was scheduled, but we can infer an
1214 // approximate start time from the expiration time.
packages/react-reconciler/src/__tests__/ReactSuspenseList-test.internal.js
+3 -4
@@ -578,7 +578,7 @@ describe('ReactSuspenseList', () => {
578
579 await B.resolve();
580
581 - expect(Scheduler).toFlushAndYield(['B']);
581 + expect(Scheduler).toFlushAndYield(['B', 'Suspend! [C]']);
582
583 // Even though we could now show B, we're still waiting on C.
584 expect(ReactNoop).toMatchRenderedOutput(
@@ -780,7 +780,6 @@ describe('ReactSuspenseList', () => {
780 'Suspend! [C]',
781 'Loading C',
782 'D',
783 - 'Suspend! [E]',
783 'Loading E',
784 'Loading F',
785 ]);
@@ -861,7 +860,7 @@ describe('ReactSuspenseList', () => {
860 );
861 });
862
864 - it('displays added row at the top "together" and the bottom in "forwards" order', async () => {
863 + it('displays added row at the top "together" and the bottom in "backwards" order', async () => {
864 let A = createAsyncText('A');
865 let B = createAsyncText('B');
866 let D = createAsyncText('D');
@@ -958,7 +957,7 @@ describe('ReactSuspenseList', () => {
957
958 await F.resolve();
959
961 - expect(Scheduler).toFlushAndYield(['F']);
960 + expect(Scheduler).toFlushAndYield(['Suspend! [D]', 'F']);
961
962 // Even though we could show F, it is still in a fallback state because
963 // E is not yet resolved. We need to resolve everything in the head first.