⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions fixtures/view-transition/src/components/SwipeRecognizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,17 @@ export default function SwipeRecognizer({
);
}
function onGestureEnd(changed) {
// Reset scroll
if (changed) {
// Trigger side-effects
startTransition(action);
}
// We cancel the gesture before invoking side-effects to allow the gesture lane to fully commit
// before scheduling new updates.
Copy link
Collaborator Author

@sebmarkbage sebmarkbage Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might actually be wrong because it wouldn't know what to entangle with until later. Nevermind this works because we entangle all transitions in the same event.

if (activeGesture.current !== null) {
const cancelGesture = activeGesture.current;
activeGesture.current = null;
cancelGesture();
}
if (changed) {
// Trigger side-effects
startTransition(action);
}
}
function onScrollEnd() {
if (touchTimeline.current) {
Expand Down
26 changes: 23 additions & 3 deletions packages/react-reconciler/src/ReactFiberGestureScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type ScheduledGesture = {
rangeEnd: number, // The percentage along the timeline where the "destination" state is reached.
types: null | TransitionTypes, // Any addTransitionType call made during startGestureTransition.
running: null | RunningViewTransition, // Used to cancel the running transition after we're done.
commit: null | (() => void), // Callback to run to commit if there's a pending commit.
committing: boolean, // If the gesture was released in a committed state and should actually commit.
revertLane: Lane, // The Lane that we'll use to schedule the revert.
prev: null | ScheduledGesture, // The previous scheduled gesture in the queue for this root.
Expand Down Expand Up @@ -64,6 +65,7 @@ export function scheduleGesture(
rangeEnd: 100, // Uninitialized
types: null,
running: null,
commit: null,
committing: false,
revertLane: NoLane, // Starts uninitialized.
prev: prev,
Expand Down Expand Up @@ -164,9 +166,17 @@ export function cancelScheduledGesture(
// lane to actually commit it.
gesture.committing = true;
if (root.pendingGestures === gesture) {
// Ping the root given the new state. This is similar to pingSuspendedRoot.
// This will either schedule the gesture lane to be committed possibly from its current state.
pingGestureRoot(root);
const commitCallback = gesture.commit;
if (commitCallback !== null) {
gesture.commit = null;
// If we already have a commit prepared we can immediately commit the tree
// without rerendering.
// TODO: Consider scheduling this in a task instead of synchronously inside the last cancellation.s
commitCallback();
} else {
// Ping the root given the new state. This is similar to pingSuspendedRoot.
pingGestureRoot(root);
}
}
} else {
// If we're not going to commit this gesture we can stop the View Transition
Expand Down Expand Up @@ -235,3 +245,13 @@ export function stopCommittedGesture(root: FiberRoot) {
}
}
}

export function scheduleGestureCommit(
gesture: ScheduledGesture,
callback: () => void,
): () => void {
gesture.commit = callback;
return function () {
gesture.commit = null;
};
}
35 changes: 35 additions & 0 deletions packages/react-reconciler/src/ReactFiberPerformanceTrack.js
Original file line number Diff line number Diff line change
Expand Up @@ -1569,6 +1569,41 @@ export function logPaintYieldPhase(
}
}

export function logApplyGesturePhase(
startTime: number,
endTime: number,
debugTask: null | ConsoleTask,
): void {
if (supportsUserTiming) {
if (endTime <= startTime) {
return;
}
if (__DEV__ && debugTask) {
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
'Create Ghost Tree',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-dark',
),
);
} else {
console.timeStamp(
'Create Ghost Tree',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-dark',
);
}
}
}

export function logStartViewTransitionYieldPhase(
startTime: number,
endTime: number,
Expand Down
80 changes: 61 additions & 19 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ import {
logSuspendedYieldTime,
setCurrentTrackFromLanes,
markAllLanesInOrder,
logApplyGesturePhase,
} from './ReactFiberPerformanceTrack';

import {
Expand Down Expand Up @@ -398,7 +399,10 @@ import {
} from './ReactFiberRootScheduler';
import {getMaskedContext, getUnmaskedContext} from './ReactFiberLegacyContext';
import {logUncaughtError} from './ReactFiberErrorLogger';
import {stopCommittedGesture} from './ReactFiberGestureScheduler';
import {
scheduleGestureCommit,
stopCommittedGesture,
} from './ReactFiberGestureScheduler';
import {claimQueuedTransitionTypes} from './ReactFiberTransitionTypes';

const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
Expand Down Expand Up @@ -1542,11 +1546,10 @@ function completeRootWhenReady(
isViewTransitionEligible ||
(isGestureTransition &&
root.pendingGestures !== null &&
// If we're committing this gesture and it already has a View Transition
// running, then we don't have to wait for that gesture. We'll stop it
// when we commit.
(root.pendingGestures.running === null ||
!root.pendingGestures.committing))
// If this gesture already has a View Transition running then we don't
// have to wait on that one before proceeding. We may hold the commit
// on the gesture committing later on in completeRoot.
root.pendingGestures.running === null)
) {
// Wait for any pending View Transition (including gestures) to finish.
suspendOnActiveViewTransition(suspendedState, root.containerInfo);
Expand Down Expand Up @@ -3463,6 +3466,16 @@ function completeRoot(
if (enableProfilerTimer && enableComponentPerformanceTrack) {
// Log the previous render phase once we commit. I.e. we weren't interrupted.
setCurrentTrackFromLanes(lanes);
if (isGestureRender(lanes)) {
// Clamp the render start time in case if something else on this lane was committed
// (such as this same tree before).
if (completedRenderStartTime < gestureClampTime) {
completedRenderStartTime = gestureClampTime;
}
if (completedRenderEndTime < gestureClampTime) {
completedRenderEndTime = gestureClampTime;
}
}
if (exitStatus === RootErrored) {
logErroredRenderPhase(
completedRenderStartTime,
Expand Down Expand Up @@ -3580,7 +3593,38 @@ function completeRoot(
} else {
// If we already have a gesture running, we don't update it in place
// even if we have a new tree. Instead we wait until we can commit.
if (enableProfilerTimer && enableComponentPerformanceTrack) {
// Clamp at the render time since we're not going to finish the rest
// of this commit or apply yet.
finalizeRender(lanes, completedRenderEndTime);
}
// We are no longer committing.
pendingEffectsRoot = (null: any); // Clear for GC purposes.
pendingFinishedWork = (null: any); // Clear for GC purposes.
pendingEffectsLanes = NoLanes;
}
// Schedule the root to be committed when the gesture completes.
root.cancelPendingCommit = scheduleGestureCommit(
committingGesture,
completeRoot.bind(
null,
root,
finishedWork,
lanes,
recoverableErrors,
transitions,
didIncludeRenderPhaseUpdate,
spawnedLane,
updatedLanes,
suspendedRetryLanes,
didSkipSuspendedSiblings,
exitStatus,
suspendedState,
'Waiting for the Gesture to finish' /* suspendedCommitReason */,
completedRenderStartTime,
completedRenderEndTime,
),
);
return;
}
}
Expand Down Expand Up @@ -4368,6 +4412,15 @@ function flushGestureMutations(): void {
ReactSharedInternals.T = prevTransition;
}

if (enableProfilerTimer && enableComponentPerformanceTrack) {
recordCommitEndTime();
logApplyGesturePhase(
pendingEffectsRenderEndTime,
commitEndTime,
animatingTask,
);
}

pendingEffectsStatus = PENDING_GESTURE_ANIMATION_PHASE;
}

Expand All @@ -4385,10 +4438,11 @@ function flushGestureAnimations(): void {
const lanes = pendingEffectsLanes;

if (enableProfilerTimer && enableComponentPerformanceTrack) {
const startViewTransitionStartTime = commitEndTime;
// Update the new commitEndTime to when we started the animation.
recordCommitEndTime();
logStartViewTransitionYieldPhase(
pendingEffectsRenderEndTime,
startViewTransitionStartTime,
commitEndTime,
pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT,
animatingTask,
Expand Down Expand Up @@ -4904,18 +4958,6 @@ export function pingGestureRoot(root: FiberRoot): void {
if (gesture === null) {
return;
}
if (
root.cancelPendingCommit !== null &&
isGestureRender(pendingEffectsLanes)
) {
// We have a suspended commit which we'll discard and rerender.
// TODO: Just use this commit since it's ready to go.
const cancelPendingCommit = root.cancelPendingCommit;
if (cancelPendingCommit !== null) {
root.cancelPendingCommit = null;
cancelPendingCommit();
}
}
// Ping it for rerender and commit.
markRootPinged(root, GestureLane);
ensureRootIsScheduled(root);
Expand Down
Loading