Skip to content

Commit 933c664

Browse files
authored
SuspenseList Optimizations (facebook#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
1 parent fbbbea1 commit 933c664

File tree

7 files changed

+386
-256
lines changed

7 files changed

+386
-256
lines changed

packages/react-reconciler/src/ReactChildFiber.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import warningWithoutStack from 'shared/warningWithoutStack';
3333

3434
import {
3535
createWorkInProgress,
36+
resetWorkInProgress,
3637
createFiberFromElement,
3738
createFiberFromFragment,
3839
createFiberFromText,
@@ -1386,3 +1387,15 @@ export function cloneChildFibers(
13861387
}
13871388
newChild.sibling = null;
13881389
}
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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,79 @@ export function createWorkInProgress(
481481
return workInProgress;
482482
}
483483

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+
484557
export function createHostRootFiber(tag: RootTag): Fiber {
485558
let mode;
486559
if (tag === ConcurrentRoot) {

0 commit comments

Comments
 (0)