Skip to content

Commit 5c5a621

Browse files
committed
Bailout of consumer updates using bitmask
The context type defines an optional function that compares two context values, returning a bitfield. A consumer may specify the bits it needs for rendering. If a provider's context changes, and the consumer's bits do not intersect with the changed bits, we can skip the consumer. This is similar to how selectors are used in Redux but fast enough to do while scanning the tree. The only user code involved is the function that computes the changed bits. But that's only called once per provider update, not for every consumer.
1 parent 10a11ef commit 5c5a621

File tree

9 files changed

+187
-64
lines changed

9 files changed

+187
-64
lines changed

packages/react-dom/src/server/ReactPartialRenderer.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
REACT_RETURN_TYPE,
3131
REACT_PORTAL_TYPE,
3232
REACT_PROVIDER_TYPE,
33-
REACT_CONSUMER_TYPE,
33+
REACT_CONTEXT_TYPE,
3434
} from 'shared/ReactSymbols';
3535

3636
import {
@@ -755,15 +755,15 @@ class ReactDOMServerRenderer {
755755
this.stack.push(frame);
756756
return '';
757757
}
758-
case REACT_CONSUMER_TYPE: {
758+
case REACT_CONTEXT_TYPE: {
759759
const consumer: ReactConsumer<any> = (nextChild: any);
760760
const nextProps = consumer.props;
761761

762-
const provider = consumer.type.context.currentProvider;
762+
const provider = consumer.type.currentProvider;
763763
let nextValue;
764764
if (provider === null) {
765765
// Detached consumer
766-
nextValue = consumer.type.context.defaultValue;
766+
nextValue = consumer.type.defaultValue;
767767
} else {
768768
nextValue = provider.props.value;
769769
}

packages/react-reconciler/src/ReactFiber.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import {
3838
REACT_RETURN_TYPE,
3939
REACT_CALL_TYPE,
4040
REACT_PROVIDER_TYPE,
41-
REACT_CONSUMER_TYPE,
41+
REACT_CONTEXT_TYPE,
4242
} from 'shared/ReactSymbols';
4343

4444
let hasBadMapPolyfill;
@@ -339,7 +339,8 @@ export function createFiberFromElement(
339339
case REACT_PROVIDER_TYPE:
340340
fiberTag = ProviderComponent;
341341
break;
342-
case REACT_CONSUMER_TYPE:
342+
case REACT_CONTEXT_TYPE:
343+
// This is a consumer
343344
fiberTag = ConsumerComponent;
344345
break;
345346
default:

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,7 @@
88
*/
99

1010
import type {HostConfig} from 'react-reconciler';
11-
import type {
12-
ReactProviderType,
13-
ReactConsumerType,
14-
ReactContext,
15-
} from 'shared/ReactTypes';
11+
import type {ReactProviderType, ReactContext} from 'shared/ReactTypes';
1612
import type {Fiber} from 'react-reconciler/src/ReactFiber';
1713
import type {HostContext} from './ReactFiberHostContext';
1814
import type {HydrationContext} from './ReactFiberHydrationContext';
@@ -68,6 +64,7 @@ import {
6864
} from './ReactFiberContext';
6965
import {pushProvider} from './ReactFiberNewContext';
7066
import {NoWork, Never} from './ReactFiberExpirationTime';
67+
import MAX_SIGNED_32_BIT_INT from './maxSigned32BitInt';
7168

7269
let warnedAboutStatelessRefs;
7370

@@ -616,6 +613,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
616613
function propagateContextChange<V>(
617614
workInProgress: Fiber,
618615
context: ReactContext<V>,
616+
changedBits: number,
619617
renderExpirationTime: ExpirationTime,
620618
): void {
621619
if (enableNewContextAPI) {
@@ -626,7 +624,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
626624
switch (fiber.tag) {
627625
case ConsumerComponent:
628626
// Check if the context matches.
629-
if (fiber.type.context === context) {
627+
const bits = fiber.stateNode;
628+
if (fiber.type === context && (bits & changedBits) !== 0) {
630629
// Update the expiration time of all the ancestors, including
631630
// the alternates.
632631
let node = fiber;
@@ -668,7 +667,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
668667
break;
669668
case ProviderComponent:
670669
// Don't scan deeper if this is a matching provider
671-
nextFiber = fiber.type === context ? null : fiber.child;
670+
nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
672671
break;
673672
default:
674673
// Traverse down.
@@ -724,12 +723,33 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
724723
workInProgress.memoizedProps = newProps;
725724

726725
const newValue = newProps.value;
727-
const oldValue = oldProps !== null ? oldProps.value : null;
728726

729-
// Use Object.is to compare the new context value to the old value.
730-
if (!is(newValue, oldValue)) {
731-
propagateContextChange(workInProgress, context, renderExpirationTime);
727+
let changedBits: number;
728+
if (oldProps === null) {
729+
// Initial render
730+
changedBits = MAX_SIGNED_32_BIT_INT;
731+
} else {
732+
const oldValue = oldProps.value;
733+
// Use Object.is to compare the new context value to the old value.
734+
if (!is(newValue, oldValue)) {
735+
changedBits =
736+
context.calculateChangedBits !== null
737+
? context.calculateChangedBits(oldValue, newValue)
738+
: MAX_SIGNED_32_BIT_INT;
739+
if (changedBits !== 0) {
740+
propagateContextChange(
741+
workInProgress,
742+
context,
743+
changedBits,
744+
renderExpirationTime,
745+
);
746+
}
747+
} else {
748+
// No change.
749+
changedBits = 0;
750+
}
732751
}
752+
workInProgress.stateNode = changedBits;
733753

734754
if (oldProps !== null && oldProps.children === newProps.children) {
735755
return bailoutOnAlreadyFinishedWork(current, workInProgress);
@@ -748,8 +768,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
748768
renderExpirationTime,
749769
) {
750770
if (enableNewContextAPI) {
751-
const consumerType: ReactConsumerType<any> = workInProgress.type;
752-
const context: ReactContext<any> = consumerType.context;
771+
const context: ReactContext<any> = workInProgress.type;
753772

754773
const newProps = workInProgress.pendingProps;
755774
const oldProps = workInProgress.memoizedProps;
@@ -758,12 +777,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
758777
const providerFiber: Fiber | null = context.currentProvider;
759778

760779
let newValue;
761-
let valueDidChange;
780+
let changedBits;
762781
if (providerFiber === null) {
763782
// This is a detached consumer (has no provider). Use the default
764783
// context value.
765784
newValue = context.defaultValue;
766-
valueDidChange = false;
785+
changedBits = 0;
767786
} else {
768787
const provider = providerFiber.pendingProps;
769788
invariant(
@@ -772,35 +791,31 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
772791
'a bug in React. Please file an issue.',
773792
);
774793
newValue = provider.value;
775-
776-
// Context change propagation stops at matching consumers, for time-
777-
// slicing. Continue the propagation here.
778-
if (oldProps === null) {
779-
valueDidChange = true;
780-
propagateContextChange(workInProgress, context, renderExpirationTime);
781-
} else {
782-
const oldValue = oldProps !== null ? oldProps.__memoizedValue : null;
783-
// Use Object.is to compare the new context value to the old value.
784-
if (!is(newValue, oldValue)) {
785-
valueDidChange = true;
786-
propagateContextChange(
787-
workInProgress,
788-
context,
789-
renderExpirationTime,
790-
);
791-
}
794+
changedBits = providerFiber.stateNode;
795+
if (changedBits !== 0) {
796+
// Context change propagation stops at matching consumers, for time-
797+
// slicing. Continue the propagation here.
798+
propagateContextChange(
799+
workInProgress,
800+
context,
801+
changedBits,
802+
renderExpirationTime,
803+
);
792804
}
793805
}
794806

795-
// The old context value is stored on the consumer object. We can't use the
796-
// provider's memoizedProps because those have already been updated by the
797-
// time we get here, in the provider's begin phase.
798-
newProps.__memoizedValue = newValue;
807+
// Store the bits on the fiber's stateNode for quick access.
808+
let bits = newProps.bits;
809+
if (bits === undefined || bits === null) {
810+
// Subscribe to all changes by default
811+
bits = MAX_SIGNED_32_BIT_INT;
812+
}
813+
workInProgress.stateNode = bits;
799814

800815
if (hasLegacyContextChanged()) {
801816
// Normally we can bail out on props equality but if context has changed
802817
// we don't do the bailout and we have to reuse existing props instead.
803-
} else if (newProps === oldProps && !valueDidChange) {
818+
} else if (newProps === oldProps && changedBits === 0) {
804819
return bailoutOnAlreadyFinishedWork(current, workInProgress);
805820
}
806821
const newChildren = newProps.render(newValue);

packages/react-reconciler/src/ReactFiberExpirationTime.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
* @flow
88
*/
99

10+
import MAX_SIGNED_32_BIT_INT from './maxSigned32BitInt';
11+
1012
// TODO: Use an opaque type once ESLint et al support the syntax
1113
export type ExpirationTime = number;
1214

1315
export const NoWork = 0;
1416
export const Sync = 1;
15-
export const Never = 2147483647; // Max int32: Math.pow(2, 31) - 1
17+
export const Never = MAX_SIGNED_32_BIT_INT;
1618

1719
const UNIT_SIZE = 10;
1820
const MAGIC_NUMBER_OFFSET = 2;

packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,80 @@ describe('ReactNewContext', () => {
425425
]);
426426
});
427427

428+
it('can skip consumers with bitmask', () => {
429+
const Context = React.unstable_createContext({foo: 0, bar: 0}, (a, b) => {
430+
let result = 0;
431+
if (a.foo !== b.foo) {
432+
result |= 0b01;
433+
}
434+
if (a.bar !== b.bar) {
435+
result |= 0b10;
436+
}
437+
return result;
438+
});
439+
440+
function Provider(props) {
441+
return Context.provide({foo: props.foo, bar: props.bar}, props.children);
442+
}
443+
444+
function Foo() {
445+
return Context.consume(value => {
446+
ReactNoop.yield('Foo');
447+
return <span prop={'Foo: ' + value.foo} />;
448+
}, 0b01);
449+
}
450+
451+
function Bar() {
452+
return Context.consume(value => {
453+
ReactNoop.yield('Bar');
454+
return <span prop={'Bar: ' + value.bar} />;
455+
}, 0b10);
456+
}
457+
458+
class Indirection extends React.Component {
459+
shouldComponentUpdate() {
460+
return false;
461+
}
462+
render() {
463+
return this.props.children;
464+
}
465+
}
466+
467+
function App(props) {
468+
return (
469+
<Provider foo={props.foo} bar={props.bar}>
470+
<Indirection>
471+
<Indirection>
472+
<Foo />
473+
</Indirection>
474+
<Indirection>
475+
<Bar />
476+
</Indirection>
477+
</Indirection>
478+
</Provider>
479+
);
480+
}
481+
482+
ReactNoop.render(<App foo={1} bar={1} />);
483+
expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']);
484+
expect(ReactNoop.getChildren()).toEqual([span('Foo: 1'), span('Bar: 1')]);
485+
486+
// Update only foo
487+
ReactNoop.render(<App foo={2} bar={1} />);
488+
expect(ReactNoop.flush()).toEqual(['Foo']);
489+
expect(ReactNoop.getChildren()).toEqual([span('Foo: 2'), span('Bar: 1')]);
490+
491+
// Update only bar
492+
ReactNoop.render(<App foo={2} bar={2} />);
493+
expect(ReactNoop.flush()).toEqual(['Bar']);
494+
expect(ReactNoop.getChildren()).toEqual([span('Foo: 2'), span('Bar: 2')]);
495+
496+
// Update both
497+
ReactNoop.render(<App foo={3} bar={3} />);
498+
expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']);
499+
expect(ReactNoop.getChildren()).toEqual([span('Foo: 3'), span('Bar: 3')]);
500+
});
501+
428502
describe('fuzz test', () => {
429503
const contextKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
430504
const contexts = new Map(
@@ -521,6 +595,7 @@ describe('ReactNewContext', () => {
521595
/>
522596
</Fragment>
523597
),
598+
null,
524599
i,
525600
);
526601
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Copyright (c) 2013-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
// The maximum safe integer for bitwise operations.
11+
// Math.pow(2, 31) - 1
12+
// 0b1111111111111111111111111111111
13+
export default 2147483647;

0 commit comments

Comments
 (0)