Skip to content

Commit a77c31c

Browse files
committed
Mark trees that need propagation in readContext
Instead of storing matched context consumers in a Set, we can mark when a consumer receives an update inside `readContext`. I hesistated to put anything in this function because it's such a hot path, but so are bail outs. Fortunately, we only need to set this flag once, the first time a context is read. So I think it's a reasonable trade off. In exchange, propagation is faster because we no longer need to accumulate a Set of matched consumers, and fiber bailouts are faster because we don't need to consult that Set. And the code is simpler.
1 parent f1e8d98 commit a77c31c

File tree

5 files changed

+96
-191
lines changed

5 files changed

+96
-191
lines changed

packages/react-reconciler/src/ReactFiberFlags.js

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,50 +12,51 @@ import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags';
1212
export type Flags = number;
1313

1414
// Don't change these two values. They're used by React Dev Tools.
15-
export const NoFlags = /* */ 0b000000000000000000000;
16-
export const PerformedWork = /* */ 0b000000000000000000001;
15+
export const NoFlags = /* */ 0b0000000000000000000000;
16+
export const PerformedWork = /* */ 0b0000000000000000000001;
1717

1818
// You can change the rest (and add more).
19-
export const Placement = /* */ 0b000000000000000000010;
20-
export const Update = /* */ 0b000000000000000000100;
19+
export const Placement = /* */ 0b0000000000000000000010;
20+
export const Update = /* */ 0b0000000000000000000100;
2121
export const PlacementAndUpdate = /* */ Placement | Update;
22-
export const Deletion = /* */ 0b000000000000000001000;
23-
export const ChildDeletion = /* */ 0b000000000000000010000;
24-
export const ContentReset = /* */ 0b000000000000000100000;
25-
export const Callback = /* */ 0b000000000000001000000;
26-
export const DidCapture = /* */ 0b000000000000010000000;
27-
export const Ref = /* */ 0b000000000000100000000;
28-
export const Snapshot = /* */ 0b000000000001000000000;
29-
export const Passive = /* */ 0b000000000010000000000;
30-
export const Hydrating = /* */ 0b000000000100000000000;
22+
export const Deletion = /* */ 0b0000000000000000001000;
23+
export const ChildDeletion = /* */ 0b0000000000000000010000;
24+
export const ContentReset = /* */ 0b0000000000000000100000;
25+
export const Callback = /* */ 0b0000000000000001000000;
26+
export const DidCapture = /* */ 0b0000000000000010000000;
27+
export const Ref = /* */ 0b0000000000000100000000;
28+
export const Snapshot = /* */ 0b0000000000001000000000;
29+
export const Passive = /* */ 0b0000000000010000000000;
30+
export const Hydrating = /* */ 0b0000000000100000000000;
3131
export const HydratingAndUpdate = /* */ Hydrating | Update;
32-
export const Visibility = /* */ 0b000000001000000000000;
32+
export const Visibility = /* */ 0b0000000001000000000000;
3333

3434
export const LifecycleEffectMask = Passive | Update | Callback | Ref | Snapshot;
3535

3636
// Union of all commit flags (flags with the lifetime of a particular commit)
37-
export const HostEffectMask = /* */ 0b000000001111111111111;
37+
export const HostEffectMask = /* */ 0b0000000001111111111111;
3838

3939
// These are not really side effects, but we still reuse this field.
40-
export const Incomplete = /* */ 0b000000010000000000000;
41-
export const ShouldCapture = /* */ 0b000000100000000000000;
40+
export const Incomplete = /* */ 0b0000000010000000000000;
41+
export const ShouldCapture = /* */ 0b0000000100000000000000;
4242
// TODO (effects) Remove this bit once the new reconciler is synced to the old.
43-
export const PassiveUnmountPendingDev = /* */ 0b000001000000000000000;
44-
export const ForceUpdateForLegacySuspense = /* */ 0b000010000000000000000;
45-
export const DidPropagateContext = /* */ 0b000100000000000000000;
43+
export const PassiveUnmountPendingDev = /* */ 0b0000001000000000000000;
44+
export const ForceUpdateForLegacySuspense = /* */ 0b0000010000000000000000;
45+
export const DidPropagateContext = /* */ 0b0000100000000000000000;
46+
export const NeedsPropagation = /* */ 0b0001000000000000000000;
4647

4748
// Static tags describe aspects of a fiber that are not specific to a render,
4849
// e.g. a fiber uses a passive effect (even if there are no updates on this particular render).
4950
// This enables us to defer more work in the unmount case,
5051
// since we can defer traversing the tree during layout to look for Passive effects,
5152
// and instead rely on the static flag as a signal that there may be cleanup work.
52-
export const PassiveStatic = /* */ 0b001000000000000000000;
53+
export const PassiveStatic = /* */ 0b0010000000000000000000;
5354

5455
// These flags allow us to traverse to fibers that have effects on mount
5556
// without traversing the entire tree after every commit for
5657
// double invoking
57-
export const MountLayoutDev = /* */ 0b010000000000000000000;
58-
export const MountPassiveDev = /* */ 0b100000000000000000000;
58+
export const MountLayoutDev = /* */ 0b0100000000000000000000;
59+
export const MountPassiveDev = /* */ 0b1000000000000000000000;
5960

6061
// Groups of flags that are used in the commit phase to skip over trees that
6162
// don't contain effects, by checking subtreeFlags.

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

Lines changed: 35 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ import {
3333
mergeLanes,
3434
pickArbitraryLane,
3535
} from './ReactFiberLane.new';
36-
import {NoFlags, DidPropagateContext} from './ReactFiberFlags';
36+
import {
37+
NoFlags,
38+
DidPropagateContext,
39+
NeedsPropagation,
40+
} from './ReactFiberFlags';
3741

3842
import invariant from 'shared/invariant';
3943
import is from 'shared/objectIs';
@@ -292,9 +296,6 @@ function propagateContextChange_eager<T>(
292296
}
293297
dependency = dependency.next;
294298
}
295-
} else if (fiber.tag === ContextProvider) {
296-
// Don't scan deeper if this is a matching provider
297-
nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
298299
} else if (
299300
enableSuspenseServerRenderer &&
300301
fiber.tag === DehydratedFragment
@@ -409,14 +410,6 @@ function propagateContextChanges<T>(
409410
// visit them during render. We should continue propagating the
410411
// siblings, though
411412
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;
420413
}
421414

422415
// Since we already found a match, we can stop traversing the
@@ -513,29 +506,14 @@ export function propagateParentContextChangesToDeferredTree(
513506
);
514507
}
515508

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-
531509
function propagateParentContextChanges(
532510
current: Fiber,
533511
workInProgress: Fiber,
534512
renderLanes: Lanes,
535513
forcePropagateEntireTree: boolean,
536514
) {
537515
if (!enableLazyContextPropagation) {
538-
return false;
516+
return;
539517
}
540518

541519
// Collect all the parent providers that changed. Since this is usually small
@@ -544,58 +522,36 @@ function propagateParentContextChanges(
544522
let parent = workInProgress;
545523
let isInsidePropagationBailout = false;
546524
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-
553525
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-
}
526+
if ((parent.flags & NeedsPropagation) !== NoFlags) {
527+
isInsidePropagationBailout = true;
528+
} else if ((parent.flags & DidPropagateContext) !== NoFlags) {
529+
break;
577530
}
578531
}
579532

580533
if (parent.tag === ContextProvider) {
581-
if (currentParent !== null) {
582-
const oldProps = currentParent.memoizedProps;
583-
if (oldProps !== null) {
584-
const providerType: ReactProviderType<any> = parent.type;
585-
const context: ReactContext<any> = providerType._context;
586-
587-
const newProps = parent.pendingProps;
588-
const newValue = newProps.value;
589-
590-
const oldValue = oldProps.value;
591-
592-
const changedBits = calculateChangedBits(context, newValue, oldValue);
593-
if (changedBits !== 0) {
594-
if (contexts !== null) {
595-
contexts.push(context, changedBits);
596-
} else {
597-
contexts = [context, changedBits];
598-
}
534+
const currentParent = parent.alternate;
535+
invariant(
536+
currentParent !== null,
537+
'Should have a current fiber. This is a bug in React.',
538+
);
539+
const oldProps = currentParent.memoizedProps;
540+
if (oldProps !== null) {
541+
const providerType: ReactProviderType<any> = parent.type;
542+
const context: ReactContext<any> = providerType._context;
543+
544+
const newProps = parent.pendingProps;
545+
const newValue = newProps.value;
546+
547+
const oldValue = oldProps.value;
548+
549+
const changedBits = calculateChangedBits(context, newValue, oldValue);
550+
if (changedBits !== 0) {
551+
if (contexts !== null) {
552+
contexts.push(context, changedBits);
553+
} else {
554+
contexts = [context, changedBits];
599555
}
600556
}
601557
}
@@ -628,10 +584,10 @@ function propagateParentContextChanges(
628584
//
629585
// Unfortunately, though, we need to ignore this flag when we're inside a
630586
// tree whose context propagation was deferred — that's what the
631-
// `deferredPropagation` set is for.
587+
// `NeedsPropagation` flag is for.
632588
//
633-
// If we could instead bail out before entering the siblings' beging phase,
634-
// then we could remove both `DidPropagateContext` and `deferredPropagation`.
589+
// If we could instead bail out before entering the siblings' begin phase,
590+
// then we could remove both `DidPropagateContext` and `NeedsPropagation`.
635591
// Consider this as part of the next refactor to the fiber tree structure.
636592
workInProgress.flags |= DidPropagateContext;
637593
}
@@ -750,6 +706,7 @@ export function readContext<T>(
750706
// TODO: This is an old field. Delete it.
751707
responders: null,
752708
};
709+
currentlyRenderingFiber.flags |= NeedsPropagation;
753710
} else {
754711
// Append a new context item.
755712
lastContextDependency = lastContextDependency.next = contextItem;

0 commit comments

Comments
 (0)