Skip to content

Commit 45dcb97

Browse files
acdlitegnoff
andcommitted
Stop propagating at nearest dependency match
Because we now propagate all context providers in a single traversal, we can defer context propagation to a subtree without losing information about which context providers we're deferring — it's all of them. Theoretically, this is a big optimization because it means we'll never propagate to any tree that has work scheduled on it, nor will we ever propagate the same tree twice. There's an awkward case related to bailing out of the siblings of a context consumer. Because those siblings don't bail out until after they've already entered the begin phase, we have to do extra work to make sure they don't unecessarily propagate context again. We could avoid this by adding an earlier bailout for sibling nodes, something we've discussed in the past. We should consider this during the next refactor of the fiber tree structure. Co-Authored-By: Josh Story <[email protected]>
1 parent 8f5f6fc commit 45dcb97

File tree

6 files changed

+502
-28
lines changed

6 files changed

+502
-28
lines changed

packages/react-reconciler/src/ReactFiberNewContext.new.js

Lines changed: 128 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,15 @@ export function propagateContextChange<T>(
197197
renderLanes: Lanes,
198198
): void {
199199
if (enableLazyContextPropagation) {
200+
// TODO: This path is only used by Cache components. Update
201+
// lazilyPropagateParentContextChanges to look for Cache components so they
202+
// can take advantage of lazy propagation.
203+
const forcePropagateEntireTree = true;
200204
propagateContextChanges(
201205
workInProgress,
202206
[context, changedBits],
203207
renderLanes,
208+
forcePropagateEntireTree,
204209
);
205210
} else {
206211
propagateContextChange_eager(
@@ -349,6 +354,7 @@ function propagateContextChanges<T>(
349354
workInProgress: Fiber,
350355
contexts: Array<any>,
351356
renderLanes: Lanes,
357+
forcePropagateEntireTree: boolean,
352358
): void {
353359
// Only used by lazy implemenation
354360
if (!enableLazyContextPropagation) {
@@ -397,6 +403,22 @@ function propagateContextChanges<T>(
397403
}
398404
scheduleWorkOnParentPath(consumer.return, renderLanes);
399405

406+
if (!forcePropagateEntireTree) {
407+
// During lazy propagation, when we find a match, we can defer
408+
// propagating changes to the children, because we're going to
409+
// visit them during render. We should continue propagating the
410+
// siblings, though
411+
nextFiber = null;
412+
413+
// Keep track of subtrees whose propagation we deferred
414+
if (deferredPropagation === null) {
415+
deferredPropagation = new Set([consumer]);
416+
} else {
417+
deferredPropagation.add(consumer);
418+
}
419+
nextFiber = null;
420+
}
421+
400422
// Since we already found a match, we can stop traversing the
401423
// dependency list.
402424
break findChangedDep;
@@ -429,7 +451,7 @@ function propagateContextChanges<T>(
429451
// on its children. We'll use the childLanes on
430452
// this fiber to indicate that a context has changed.
431453
scheduleWorkOnParentPath(parentSuspense, renderLanes);
432-
nextFiber = fiber.sibling;
454+
nextFiber = null;
433455
} else {
434456
// Traverse down.
435457
nextFiber = fiber.child;
@@ -462,14 +484,58 @@ function propagateContextChanges<T>(
462484
}
463485
}
464486

465-
// Alias for propagating a deferred tree (Suspense, Offscreen). Currently it's
466-
// the same algorithm but there may be a way to optimize one or the other.
467-
export const propagateParentContextChangesToDeferredTree = lazilyPropagateParentContextChanges;
468-
469487
export function lazilyPropagateParentContextChanges(
470488
current: Fiber,
471489
workInProgress: Fiber,
472490
renderLanes: Lanes,
491+
) {
492+
const forcePropagateEntireTree = false;
493+
propagateParentContextChanges(
494+
current,
495+
workInProgress,
496+
renderLanes,
497+
forcePropagateEntireTree,
498+
);
499+
}
500+
501+
// Used for propagating a deferred tree (Suspense, Offscreen). We must propagate
502+
// to the entire subtree, because we won't revisit it until after the current
503+
// render has completed, at which point we'll have lost track of which providers
504+
// have changed.
505+
export function propagateParentContextChangesToDeferredTree(
506+
current: Fiber,
507+
workInProgress: Fiber,
508+
renderLanes: Lanes,
509+
) {
510+
const forcePropagateEntireTree = true;
511+
propagateParentContextChanges(
512+
current,
513+
workInProgress,
514+
renderLanes,
515+
forcePropagateEntireTree,
516+
);
517+
}
518+
519+
// Used by lazy context propagation algorithm. When we find a context dependency
520+
// match, we don't propagate the changes any further into that fiber's subtree.
521+
// We add the matched fibers to this set. Later, if something inside that
522+
// subtree bails out of rendering, the presence of a parent fiber in this Set
523+
// tells us that we need to continue propagating.
524+
//
525+
// This is a set of _current_ fibers, not work-in-progress fibers. That's why
526+
// it's a set instead of a flag on the fiber.
527+
let deferredPropagation: Set<Fiber> | null = null;
528+
529+
export function resetDeferredContextPropagation() {
530+
// This is called by prepareFreshStack
531+
deferredPropagation = null;
532+
}
533+
534+
function propagateParentContextChanges(
535+
current: Fiber,
536+
workInProgress: Fiber,
537+
renderLanes: Lanes,
538+
forcePropagateEntireTree: boolean,
473539
) {
474540
if (!enableLazyContextPropagation) {
475541
return false;
@@ -479,9 +545,42 @@ export function lazilyPropagateParentContextChanges(
479545
// number, we use an Array instead of Set.
480546
let contexts = null;
481547
let parent = workInProgress;
482-
while (parent !== null && (parent.flags & DidPropagateContext) === NoFlags) {
548+
let isInsidePropagationBailout = false;
549+
while (parent !== null) {
550+
const currentParent = parent.alternate;
551+
invariant(
552+
currentParent !== null,
553+
'Should have a current fiber. This is a bug in React.',
554+
);
555+
556+
if (!isInsidePropagationBailout) {
557+
if (deferredPropagation === null) {
558+
if ((parent.flags & DidPropagateContext) !== NoFlags) {
559+
break;
560+
}
561+
} else {
562+
if (currentParent !== null && deferredPropagation.has(currentParent)) {
563+
// We're inside a subtree that previously bailed out of propagation.
564+
// We must disregard the the DidPropagateContext flag as we continue
565+
// searching for parent providers.
566+
isInsidePropagationBailout = true;
567+
// We know that none of the providers in between the propagation
568+
// bailout and the nearest render bailout above that could have
569+
// changed. So we can skip those.
570+
do {
571+
parent = parent.return;
572+
invariant(
573+
parent !== null,
574+
'Expected to find a bailed out fiber. This is a bug in React.',
575+
);
576+
} while ((parent.flags & DidPropagateContext) === NoFlags);
577+
} else if ((parent.flags & DidPropagateContext) !== NoFlags) {
578+
break;
579+
}
580+
}
581+
}
582+
483583
if (parent.tag === ContextProvider) {
484-
const currentParent = parent.alternate;
485584
if (currentParent !== null) {
486585
const oldProps = currentParent.memoizedProps;
487586
if (oldProps !== null) {
@@ -510,15 +609,33 @@ export function lazilyPropagateParentContextChanges(
510609
if (contexts !== null) {
511610
// If there were any changed providers, search through the children and
512611
// propagate their changes.
513-
propagateContextChanges(workInProgress, contexts, renderLanes);
612+
propagateContextChanges(
613+
workInProgress,
614+
contexts,
615+
renderLanes,
616+
forcePropagateEntireTree,
617+
);
514618
}
515619

516-
// This is an optimization so that we only propagate once per subtree. (We
517-
// will propagate the same providers to different subtrees, though — that's
518-
// why the flag is on the fiber that bailed out, not the provider.) If a
620+
// This is an optimization so that we only propagate once per subtree. If a
519621
// deeply nested child bails out, and it calls this propagation function, it
520622
// uses this flag to know that the remaining ancestor providers have already
521623
// been propagated.
624+
//
625+
// NOTE: This optimization is only necessary because we sometimes enter the
626+
// begin phase of nodes that don't have any work scheduled on them —
627+
// specifically, the siblings of a node that _does_ have scheduled work. The
628+
// siblings will bail out and call this function again, even though we already
629+
// propagated content changes to it and its subtree. So we use this flag to
630+
// mark that the parent providers already propagated.
631+
//
632+
// Unfortunately, though, we need to ignore this flag when we're inside a
633+
// tree whose context propagation was deferred — that's what the
634+
// `deferredPropagation` set is for.
635+
//
636+
// If we could instead bail out before entering the siblings' beging phase,
637+
// then we could remove both `DidPropagateContext` and `deferredPropagation`.
638+
// Consider this as part of the next refactor to the fiber tree structure.
522639
workInProgress.flags |= DidPropagateContext;
523640
}
524641

0 commit comments

Comments
 (0)