Skip to content

Commit deeeaf1

Browse files
authored
Entangle overlapping transitions per queue (#20670)
When multiple transitions update the same queue, only the most recent one should be allowed to finish. We shouldn't show intermediate states. See #17418 for background on why this is important. The way this currently works is that we always assign the same lane to all transitions. It's impossible for one transition to finish without also finishing all the others. The downside of the current approach is that it's too aggressive. Not all transitions are related to each other, so one should not block the other. The new approach is to only entangle transitions if they update one or more of the same state hooks (or class components), because this indicates that they are related. If they are unrelated, then they can finish in any order, as long as they have different lanes. However, this commit does not change anything about how the lanes are assigned. All it does is add the mechanism to entangle per queue. So it doesn't actually change any behavior, yet. But it's a requirement for my next step, which is to assign different lanes to consecutive transitions until we run out and cycle back to the beginning.
1 parent e316f78 commit deeeaf1

10 files changed

+216
-22
lines changed

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040

4141
import {
4242
enqueueUpdate,
43+
entangleTransitions,
4344
processUpdateQueue,
4445
checkHasForceUpdateAfterProcessing,
4546
resetHasForceUpdateBeforeProcessing,
@@ -214,7 +215,10 @@ const classComponentUpdater = {
214215
}
215216

216217
enqueueUpdate(fiber, update, lane);
217-
scheduleUpdateOnFiber(fiber, lane, eventTime);
218+
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
219+
if (root !== null) {
220+
entangleTransitions(root, fiber, lane);
221+
}
218222

219223
if (__DEV__) {
220224
if (enableDebugTracing) {
@@ -246,7 +250,10 @@ const classComponentUpdater = {
246250
}
247251

248252
enqueueUpdate(fiber, update, lane);
249-
scheduleUpdateOnFiber(fiber, lane, eventTime);
253+
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
254+
if (root !== null) {
255+
entangleTransitions(root, fiber, lane);
256+
}
250257

251258
if (__DEV__) {
252259
if (enableDebugTracing) {
@@ -277,7 +284,10 @@ const classComponentUpdater = {
277284
}
278285

279286
enqueueUpdate(fiber, update, lane);
280-
scheduleUpdateOnFiber(fiber, lane, eventTime);
287+
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
288+
if (root !== null) {
289+
entangleTransitions(root, fiber, lane);
290+
}
281291

282292
if (__DEV__) {
283293
if (enableDebugTracing) {

packages/react-reconciler/src/ReactFiberClassComponent.old.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040

4141
import {
4242
enqueueUpdate,
43+
entangleTransitions,
4344
processUpdateQueue,
4445
checkHasForceUpdateAfterProcessing,
4546
resetHasForceUpdateBeforeProcessing,
@@ -214,7 +215,10 @@ const classComponentUpdater = {
214215
}
215216

216217
enqueueUpdate(fiber, update);
217-
scheduleUpdateOnFiber(fiber, lane, eventTime);
218+
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
219+
if (root !== null) {
220+
entangleTransitions(root, fiber, lane);
221+
}
218222

219223
if (__DEV__) {
220224
if (enableDebugTracing) {
@@ -246,7 +250,10 @@ const classComponentUpdater = {
246250
}
247251

248252
enqueueUpdate(fiber, update);
249-
scheduleUpdateOnFiber(fiber, lane, eventTime);
253+
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
254+
if (root !== null) {
255+
entangleTransitions(root, fiber, lane);
256+
}
250257

251258
if (__DEV__) {
252259
if (enableDebugTracing) {
@@ -277,7 +284,10 @@ const classComponentUpdater = {
277284
}
278285

279286
enqueueUpdate(fiber, update);
280-
scheduleUpdateOnFiber(fiber, lane, eventTime);
287+
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
288+
if (root !== null) {
289+
entangleTransitions(root, fiber, lane);
290+
}
281291

282292
if (__DEV__) {
283293
if (enableDebugTracing) {

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

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ import {
4545
isSubsetOfLanes,
4646
mergeLanes,
4747
removeLanes,
48+
intersectLanes,
49+
isTransitionLane,
4850
markRootEntangled,
4951
markRootMutableRead,
5052
getCurrentUpdateLanePriority,
@@ -104,7 +106,11 @@ import {getIsRendering} from './ReactCurrentFiber';
104106
import {logStateUpdateScheduled} from './DebugTracing';
105107
import {markStateUpdateScheduled} from './SchedulingProfiler';
106108
import {CacheContext} from './ReactFiberCacheComponent.new';
107-
import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.new';
109+
import {
110+
createUpdate,
111+
enqueueUpdate,
112+
entangleTransitions,
113+
} from './ReactUpdateQueue.new';
108114
import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new';
109115

110116
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -121,6 +127,7 @@ type Update<S, A> = {|
121127
export type UpdateQueue<S, A> = {|
122128
pending: Update<S, A> | null,
123129
interleaved: Update<S, A> | null,
130+
lanes: Lanes,
124131
dispatch: (A => mixed) | null,
125132
lastRenderedReducer: ((S, A) => S) | null,
126133
lastRenderedState: S | null,
@@ -654,6 +661,7 @@ function mountReducer<S, I, A>(
654661
const queue = (hook.queue = {
655662
pending: null,
656663
interleaved: null,
664+
lanes: NoLanes,
657665
dispatch: null,
658666
lastRenderedReducer: reducer,
659667
lastRenderedState: (initialState: any),
@@ -811,6 +819,10 @@ function updateReducer<S, I, A>(
811819
markSkippedUpdateLanes(interleavedLane);
812820
interleaved = ((interleaved: any).next: Update<S, A>);
813821
} while (interleaved !== lastInterleaved);
822+
} else if (baseQueue === null) {
823+
// `queue.lanes` is used for entangling transitions. We can set it back to
824+
// zero once the queue is empty.
825+
queue.lanes = NoLanes;
814826
}
815827

816828
const dispatch: Dispatch<A> = (queue.dispatch: any);
@@ -1102,6 +1114,7 @@ function useMutableSource<Source, Snapshot>(
11021114
const newQueue = {
11031115
pending: null,
11041116
interleaved: null,
1117+
lanes: NoLanes,
11051118
dispatch: null,
11061119
lastRenderedReducer: basicStateReducer,
11071120
lastRenderedState: snapshot,
@@ -1158,6 +1171,7 @@ function mountState<S>(
11581171
const queue = (hook.queue = {
11591172
pending: null,
11601173
interleaved: null,
1174+
lanes: NoLanes,
11611175
dispatch: null,
11621176
lastRenderedReducer: basicStateReducer,
11631177
lastRenderedState: (initialState: any),
@@ -1821,6 +1835,9 @@ function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
18211835
const lane = requestUpdateLane(provider);
18221836
const eventTime = requestEventTime();
18231837
const root = scheduleUpdateOnFiber(provider, lane, eventTime);
1838+
if (root !== null) {
1839+
entangleTransitions(root, fiber, lane);
1840+
}
18241841

18251842
const seededCache = new Map();
18261843
if (seedKey !== null && seedKey !== undefined && root !== null) {
@@ -1960,7 +1977,25 @@ function dispatchAction<S, A>(
19601977
warnIfNotCurrentlyActingUpdatesInDev(fiber);
19611978
}
19621979
}
1963-
scheduleUpdateOnFiber(fiber, lane, eventTime);
1980+
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
1981+
1982+
if (isTransitionLane(lane) && root !== null) {
1983+
let queueLanes = queue.lanes;
1984+
1985+
// If any entangled lanes are no longer pending on the root, then they
1986+
// must have finished. We can remove them from the shared queue, which
1987+
// represents a superset of the actually pending lanes. In some cases we
1988+
// may entangle more than we need to, but that's OK. In fact it's worse if
1989+
// we *don't* entangle when we should.
1990+
queueLanes = intersectLanes(queueLanes, root.pendingLanes);
1991+
1992+
// Entangle the new transition lane with the other transition lanes.
1993+
const newQueueLanes = mergeLanes(queueLanes, lane);
1994+
if (newQueueLanes !== queueLanes) {
1995+
queue.lanes = newQueueLanes;
1996+
markRootEntangled(root, newQueueLanes);
1997+
}
1998+
}
19641999
}
19652000

19662001
if (__DEV__) {

packages/react-reconciler/src/ReactFiberHooks.old.js

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ import {
4545
isSubsetOfLanes,
4646
mergeLanes,
4747
removeLanes,
48+
intersectLanes,
49+
isTransitionLane,
4850
markRootEntangled,
4951
markRootMutableRead,
5052
getCurrentUpdateLanePriority,
@@ -103,7 +105,11 @@ import {getIsRendering} from './ReactCurrentFiber';
103105
import {logStateUpdateScheduled} from './DebugTracing';
104106
import {markStateUpdateScheduled} from './SchedulingProfiler';
105107
import {CacheContext} from './ReactFiberCacheComponent.old';
106-
import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.old';
108+
import {
109+
createUpdate,
110+
enqueueUpdate,
111+
entangleTransitions,
112+
} from './ReactUpdateQueue.old';
107113

108114
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
109115

@@ -118,6 +124,7 @@ type Update<S, A> = {|
118124

119125
type UpdateQueue<S, A> = {|
120126
pending: Update<S, A> | null,
127+
lanes: Lanes,
121128
dispatch: (A => mixed) | null,
122129
lastRenderedReducer: ((S, A) => S) | null,
123130
lastRenderedState: S | null,
@@ -650,6 +657,7 @@ function mountReducer<S, I, A>(
650657
hook.memoizedState = hook.baseState = initialState;
651658
const queue = (hook.queue = {
652659
pending: null,
660+
lanes: NoLanes,
653661
dispatch: null,
654662
lastRenderedReducer: reducer,
655663
lastRenderedState: (initialState: any),
@@ -792,6 +800,12 @@ function updateReducer<S, I, A>(
792800
queue.lastRenderedState = newState;
793801
}
794802

803+
if (baseQueue === null) {
804+
// `queue.lanes` is used for entangling transitions. We can set it back to
805+
// zero once the queue is empty.
806+
queue.lanes = NoLanes;
807+
}
808+
795809
const dispatch: Dispatch<A> = (queue.dispatch: any);
796810
return [hook.memoizedState, dispatch];
797811
}
@@ -1080,6 +1094,7 @@ function useMutableSource<Source, Snapshot>(
10801094
// including any interleaving updates that occur.
10811095
const newQueue = {
10821096
pending: null,
1097+
lanes: NoLanes,
10831098
dispatch: null,
10841099
lastRenderedReducer: basicStateReducer,
10851100
lastRenderedState: snapshot,
@@ -1135,6 +1150,7 @@ function mountState<S>(
11351150
hook.memoizedState = hook.baseState = initialState;
11361151
const queue = (hook.queue = {
11371152
pending: null,
1153+
lanes: NoLanes,
11381154
dispatch: null,
11391155
lastRenderedReducer: basicStateReducer,
11401156
lastRenderedState: (initialState: any),
@@ -1798,6 +1814,9 @@ function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
17981814
const lane = requestUpdateLane(provider);
17991815
const eventTime = requestEventTime();
18001816
const root = scheduleUpdateOnFiber(provider, lane, eventTime);
1817+
if (root !== null) {
1818+
entangleTransitions(root, fiber, lane);
1819+
}
18011820

18021821
const seededCache = new Map();
18031822
if (seedKey !== null && seedKey !== undefined && root !== null) {
@@ -1914,7 +1933,25 @@ function dispatchAction<S, A>(
19141933
warnIfNotCurrentlyActingUpdatesInDev(fiber);
19151934
}
19161935
}
1917-
scheduleUpdateOnFiber(fiber, lane, eventTime);
1936+
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
1937+
1938+
if (isTransitionLane(lane) && root !== null) {
1939+
let queueLanes = queue.lanes;
1940+
1941+
// If any entangled lanes are no longer pending on the root, then they
1942+
// must have finished. We can remove them from the shared queue, which
1943+
// represents a superset of the actually pending lanes. In some cases we
1944+
// may entangle more than we need to, but that's OK. In fact it's worse if
1945+
// we *don't* entangle when we should.
1946+
queueLanes = intersectLanes(queueLanes, root.pendingLanes);
1947+
1948+
// Entangle the new transition lane with the other transition lanes.
1949+
const newQueueLanes = mergeLanes(queueLanes, lane);
1950+
if (newQueueLanes !== queueLanes) {
1951+
queue.lanes = newQueueLanes;
1952+
markRootEntangled(root, newQueueLanes);
1953+
}
1954+
}
19181955
}
19191956

19201957
if (__DEV__) {

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -310,9 +310,8 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
310310
}
311311

312312
if (enableTransitionEntanglement) {
313-
// We don't need to include higher priority lanes, because in this
314-
// experiment we always unsuspend all transitions whenever we receive
315-
// an update.
313+
// We don't need to do anything extra here, because we apply per-lane
314+
// transition entanglement in the entanglement loop below.
316315
} else {
317316
// If there are higher priority lanes, we'll include them even if they
318317
// are suspended.
@@ -492,6 +491,10 @@ export function includesOnlyTransitions(lanes: Lanes) {
492491
return (lanes & TransitionLanes) === lanes;
493492
}
494493

494+
export function isTransitionLane(lane: Lane) {
495+
return (lane & TransitionLanes) !== 0;
496+
}
497+
495498
// To ensure consistency across multiple updates in the same event, this should
496499
// be a pure function, so that it always returns the same lane for given inputs.
497500
export function findUpdateLane(
@@ -634,6 +637,10 @@ export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {
634637
return set & ~subset;
635638
}
636639

640+
export function intersectLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
641+
return a & b;
642+
}
643+
637644
// Seems redundant, but it changes the type from a single lane (used for
638645
// updates) to a group of lanes (used for flushing work).
639646
export function laneToLanes(lane: Lane): Lanes {

packages/react-reconciler/src/ReactFiberLane.old.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -310,9 +310,8 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
310310
}
311311

312312
if (enableTransitionEntanglement) {
313-
// We don't need to include higher priority lanes, because in this
314-
// experiment we always unsuspend all transitions whenever we receive
315-
// an update.
313+
// We don't need to do anything extra here, because we apply per-lane
314+
// transition entanglement in the entanglement loop below.
316315
} else {
317316
// If there are higher priority lanes, we'll include them even if they
318317
// are suspended.
@@ -492,6 +491,10 @@ export function includesOnlyTransitions(lanes: Lanes) {
492491
return (lanes & TransitionLanes) === lanes;
493492
}
494493

494+
export function isTransitionLane(lane: Lane) {
495+
return (lane & TransitionLanes) !== 0;
496+
}
497+
495498
// To ensure consistency across multiple updates in the same event, this should
496499
// be a pure function, so that it always returns the same lane for given inputs.
497500
export function findUpdateLane(
@@ -634,6 +637,10 @@ export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {
634637
return set & ~subset;
635638
}
636639

640+
export function intersectLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
641+
return a & b;
642+
}
643+
637644
// Seems redundant, but it changes the type from a single lane (used for
638645
// updates) to a group of lanes (used for flushing work).
639646
export function laneToLanes(lane: Lane): Lanes {

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@ import {
6565
IsThisRendererActing,
6666
act,
6767
} from './ReactFiberWorkLoop.new';
68-
import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.new';
68+
import {
69+
createUpdate,
70+
enqueueUpdate,
71+
entangleTransitions,
72+
} from './ReactUpdateQueue.new';
6973
import {
7074
isRendering as ReactCurrentFiberIsRendering,
7175
current as ReactCurrentFiberCurrent,
@@ -315,7 +319,10 @@ export function updateContainer(
315319
}
316320

317321
enqueueUpdate(current, update, lane);
318-
scheduleUpdateOnFiber(current, lane, eventTime);
322+
const root = scheduleUpdateOnFiber(current, lane, eventTime);
323+
if (root !== null) {
324+
entangleTransitions(root, current, lane);
325+
}
319326

320327
return lane;
321328
}

0 commit comments

Comments
 (0)