Skip to content

Commit f1e8d98

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 449a007 commit f1e8d98

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;
@@ -426,7 +448,7 @@ function propagateContextChanges<T>(
426448
// on its children. We'll use the childLanes on
427449
// this fiber to indicate that a context has changed.
428450
scheduleWorkOnParentPath(parentSuspense, renderLanes);
429-
nextFiber = fiber.sibling;
451+
nextFiber = null;
430452
} else {
431453
// Traverse down.
432454
nextFiber = fiber.child;
@@ -459,14 +481,58 @@ function propagateContextChanges<T>(
459481
}
460482
}
461483

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

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

0 commit comments

Comments
 (0)