Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Synchronous popstate transitions #30759

Merged
merged 1 commit into from
Aug 23, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,7 @@ describe('ReactDOMFiberAsync', () => {
// Because it suspended, it remains on the current path
expect(div.textContent).toBe('/path/a');
});
assertLog(['Suspend! [/path/b]']);
assertLog([]);

await act(async () => {
resolvePromise();
Expand Down
72 changes: 61 additions & 11 deletions packages/react-reconciler/src/ReactFiberLane.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ export const DeferredLane: Lane = /* */ 0b1000000000000000000
export const UpdateLanes: Lanes =
SyncLane | InputContinuousLane | DefaultLane | TransitionLanes;

export const HydrationLanes =
SyncHydrationLane |
InputContinuousHydrationLane |
DefaultHydrationLane |
TransitionHydrationLane |
SelectiveHydrationLane |
IdleHydrationLane;

// This function is used for the experimental timeline (react-devtools-timeline)
// It should be kept in sync with the Lanes values above.
export function getLabelForLane(lane: Lane): string | void {
Expand Down Expand Up @@ -282,6 +290,51 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
return nextLanes;
}

export function getNextLanesToFlushSync(
root: FiberRoot,
extraLanesToForceSync: Lane | Lanes,
): Lanes {
// Similar to getNextLanes, except instead of choosing the next lanes to work
// on based on their priority, it selects all the lanes that have equal or
// higher priority than those are given. That way they can be synchronously
// rendered in a single batch.
//
// The main use case is updates scheduled by popstate events, which are
// flushed synchronously even though they are transitions.
const lanesToFlush = SyncUpdateLanes | extraLanesToForceSync;

// Early bailout if there's no pending work left.
const pendingLanes = root.pendingLanes;
if (pendingLanes === NoLanes) {
return NoLanes;
}

const suspendedLanes = root.suspendedLanes;
const pingedLanes = root.pingedLanes;

// Remove lanes that are suspended (but not pinged)
const unblockedLanes = pendingLanes & ~(suspendedLanes & ~pingedLanes);
const unblockedLanesWithMatchingPriority =
unblockedLanes & getLanesOfEqualOrHigherPriority(lanesToFlush);

// If there are matching hydration lanes, we should do those by themselves.
// Hydration lanes must never include updates.
if (unblockedLanesWithMatchingPriority & HydrationLanes) {
return (
(unblockedLanesWithMatchingPriority & HydrationLanes) | SyncHydrationLane
);
}

if (unblockedLanesWithMatchingPriority) {
// Always include the SyncLane as part of the result, even if there's no
// pending sync work, to indicate the priority of the entire batch of work
// is considered Sync.
return unblockedLanesWithMatchingPriority | SyncLane;
}

return NoLanes;
}

export function getEntangledLanes(root: FiberRoot, renderLanes: Lanes): Lanes {
let entangledLanes = renderLanes;

Expand Down Expand Up @@ -534,6 +587,14 @@ export function getHighestPriorityLane(lanes: Lanes): Lane {
return lanes & -lanes;
}

function getLanesOfEqualOrHigherPriority(lanes: Lane | Lanes): Lanes {
// Create a mask with all bits to the right or same as the highest bit.
// So if lanes is 0b100, the result would be 0b111.
// If lanes is 0b101, the result would be 0b111.
const lowestPriorityLaneIndex = 31 - clz32(lanes);
return (1 << (lowestPriorityLaneIndex + 1)) - 1;
}
Comment on lines +590 to +596
Copy link
Collaborator

Choose a reason for hiding this comment

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

That makes sense to me now 👍🏻


export function pickArbitraryLane(lanes: Lanes): Lane {
// This wrapper function gets inlined. Only exists so to communicate that it
// doesn't matter which bit is selected; you can pick any bit without
Expand Down Expand Up @@ -757,17 +818,6 @@ export function markRootEntangled(root: FiberRoot, entangledLanes: Lanes) {
}
}

export function upgradePendingLaneToSync(root: FiberRoot, lane: Lane) {
// Since we're upgrading the priority of the given lane, there is now pending
// sync work.
root.pendingLanes |= SyncLane;

// Entangle the sync lane with the lane we're upgrading. This means SyncLane
// will not be allowed to finish without also finishing the given lane.
root.entangledLanes |= SyncLane;
root.entanglements[SyncLaneIndex] |= lane;
}

export function upgradePendingLanesToSync(
root: FiberRoot,
lanesToUpgrade: Lanes,
Expand Down
85 changes: 54 additions & 31 deletions packages/react-reconciler/src/ReactFiberRootScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

import type {FiberRoot} from './ReactInternalTypes';
import type {Lane} from './ReactFiberLane';
import type {Lane, Lanes} from './ReactFiberLane';
import type {PriorityLevel} from 'scheduler/src/SchedulerPriorities';
import type {BatchConfigTransition} from './ReactFiberTracingMarkerComponent';

Expand All @@ -24,8 +24,8 @@ import {
getNextLanes,
includesSyncLane,
markStarvedLanesAsExpired,
upgradePendingLaneToSync,
claimNextTransitionLane,
getNextLanesToFlushSync,
} from './ReactFiberLane';
import {
CommitContext,
Expand Down Expand Up @@ -145,18 +145,21 @@ export function ensureRootIsScheduled(root: FiberRoot): void {
export function flushSyncWorkOnAllRoots() {
// This is allowed to be called synchronously, but the caller should check
// the execution context first.
flushSyncWorkAcrossRoots_impl(false);
flushSyncWorkAcrossRoots_impl(NoLanes, false);
}

export function flushSyncWorkOnLegacyRootsOnly() {
// This is allowed to be called synchronously, but the caller should check
// the execution context first.
if (!disableLegacyMode) {
flushSyncWorkAcrossRoots_impl(true);
flushSyncWorkAcrossRoots_impl(NoLanes, true);
}
}

function flushSyncWorkAcrossRoots_impl(onlyLegacy: boolean) {
function flushSyncWorkAcrossRoots_impl(
syncTransitionLanes: Lanes | Lane,
onlyLegacy: boolean,
) {
if (isFlushingWork) {
// Prevent reentrancy.
// TODO: Is this overly defensive? The callers must check the execution
Expand All @@ -179,17 +182,28 @@ function flushSyncWorkAcrossRoots_impl(onlyLegacy: boolean) {
if (onlyLegacy && (disableLegacyMode || root.tag !== LegacyRoot)) {
// Skip non-legacy roots.
} else {
const workInProgressRoot = getWorkInProgressRoot();
const workInProgressRootRenderLanes =
getWorkInProgressRootRenderLanes();
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
if (includesSyncLane(nextLanes)) {
// This root has pending sync work. Flush it now.
didPerformSomeWork = true;
performSyncWorkOnRoot(root, nextLanes);
if (syncTransitionLanes !== NoLanes) {
const nextLanes = getNextLanesToFlushSync(root, syncTransitionLanes);
if (nextLanes !== NoLanes) {
// This root has pending sync work. Flush it now.
didPerformSomeWork = true;
performSyncWorkOnRoot(root, nextLanes);
}
} else {
const workInProgressRoot = getWorkInProgressRoot();
const workInProgressRootRenderLanes =
getWorkInProgressRootRenderLanes();
const nextLanes = getNextLanes(
root,
root === workInProgressRoot
? workInProgressRootRenderLanes
: NoLanes,
);
if (includesSyncLane(nextLanes)) {
// This root has pending sync work. Flush it now.
didPerformSomeWork = true;
performSyncWorkOnRoot(root, nextLanes);
}
}
}
root = root.next;
Expand All @@ -209,23 +223,23 @@ function processRootScheduleInMicrotask() {
// We'll recompute this as we iterate through all the roots and schedule them.
mightHavePendingSyncWork = false;

let syncTransitionLanes = NoLanes;
if (currentEventTransitionLane !== NoLane) {
if (shouldAttemptEagerTransition()) {
// A transition was scheduled during an event, but we're going to try to
// render it synchronously anyway. We do this during a popstate event to
// preserve the scroll position of the previous page.
syncTransitionLanes = currentEventTransitionLane;
}
currentEventTransitionLane = NoLane;
}

const currentTime = now();

let prev = null;
let root = firstScheduledRoot;
while (root !== null) {
const next = root.next;

if (
currentEventTransitionLane !== NoLane &&
shouldAttemptEagerTransition()
) {
// A transition was scheduled during an event, but we're going to try to
// render it synchronously anyway. We do this during a popstate event to
// preserve the scroll position of the previous page.
upgradePendingLaneToSync(root, currentEventTransitionLane);
}

const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime);
if (nextLanes === NoLane) {
// This root has no more pending work. Remove it from the schedule. To
Expand All @@ -248,18 +262,27 @@ function processRootScheduleInMicrotask() {
} else {
// This root still has work. Keep it in the list.
prev = root;
if (includesSyncLane(nextLanes)) {

// This is a fast-path optimization to early exit from
// flushSyncWorkOnAllRoots if we can be certain that there is no remaining
// synchronous work to perform. Set this to true if there might be sync
// work left.
if (
// Skip the optimization if syncTransitionLanes is set
syncTransitionLanes !== NoLanes ||
// Common case: we're not treating any extra lanes as synchronous, so we
// can just check if the next lanes are sync.
includesSyncLane(nextLanes)
) {
mightHavePendingSyncWork = true;
}
}
root = next;
}

currentEventTransitionLane = NoLane;

// At the end of the microtask, flush any pending synchronous work. This has
// to come at the end, because it does actual rendering work that might throw.
flushSyncWorkOnAllRoots();
flushSyncWorkAcrossRoots_impl(syncTransitionLanes, false);
}

function scheduleTaskForRootDuringMicrotask(
Expand Down
Loading