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
2 changes: 1 addition & 1 deletion packages/react-art/src/ReactFiberConfigART.js
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@ export function suspendInstance(instance, type, props) {}

export function suspendOnActiveViewTransition(container) {}

export function waitForCommitToBeReady() {
export function waitForCommitToBeReady(timeoutOffset) {
return null;
}

Expand Down
68 changes: 49 additions & 19 deletions packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -5905,7 +5905,9 @@ export function preloadResource(resource: Resource): boolean {

type SuspendedState = {
stylesheets: null | Map<StylesheetResource, HoistableRoot>,
count: number,
count: number, // suspensey css and active view transitions
imgCount: number, // suspensey images
waitingForImages: boolean, // false when we're no longer blocking on images
unsuspend: null | (() => void),
};
let suspendedState: null | SuspendedState = null;
Expand All @@ -5914,6 +5916,8 @@ export function startSuspendingCommit(): void {
suspendedState = {
stylesheets: null,
count: 0,
imgCount: 0,
waitingForImages: true,
// We use a noop function when we begin suspending because if possible we want the
// waitfor step to finish synchronously. If it doesn't we'll return a function to
// provide the actual unsuspend function and that will get completed when the count
Expand All @@ -5922,6 +5926,8 @@ export function startSuspendingCommit(): void {
};
}

const SUSPENSEY_STYLESHEET_TIMEOUT = 60000;

const SUSPENSEY_IMAGE_TIMEOUT = 500;

export function suspendInstance(
Expand All @@ -5946,13 +5952,10 @@ export function suspendInstance(
// If this browser supports decode() API, we use it to suspend waiting on the image.
// The loading should have already started at this point, so it should be enough to
// just call decode() which should also wait for the data to finish loading.
state.count++;
const ping = onUnsuspend.bind(state);
Promise.race([
// $FlowFixMe[prop-missing]
instance.decode(),
new Promise(resolve => setTimeout(resolve, SUSPENSEY_IMAGE_TIMEOUT)),
]).then(ping, ping);
state.imgCount++;
const ping = onUnsuspendImg.bind(state);
// $FlowFixMe[prop-missing]
instance.decode().then(ping, ping);
}
}

Expand Down Expand Up @@ -6067,7 +6070,9 @@ export function suspendOnActiveViewTransition(rootContainer: Container): void {
activeViewTransition.finished.then(ping, ping);
}

export function waitForCommitToBeReady(): null | ((() => void) => () => void) {
export function waitForCommitToBeReady(
timeoutOffset: number,
): null | ((() => void) => () => void) {
if (suspendedState === null) {
throw new Error(
'Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug.',
Expand All @@ -6085,7 +6090,7 @@ export function waitForCommitToBeReady(): null | ((() => void) => () => void) {

// We need to check the count again because the inserted stylesheets may have led to new
// tasks to wait on.
if (state.count > 0) {
if (state.count > 0 || state.imgCount > 0) {
return commit => {
// We almost never want to show content before its styles have loaded. But
// eventually we will give up and allow unstyled content. So this number is
Expand All @@ -6102,37 +6107,62 @@ export function waitForCommitToBeReady(): null | ((() => void) => () => void) {
state.unsuspend = null;
unsuspend();
}
}, 60000); // one minute
}, SUSPENSEY_STYLESHEET_TIMEOUT + timeoutOffset);

const imgTimer = setTimeout(() => {
// We're no longer blocked on images. If CSS resolves after this we can commit.
state.waitingForImages = false;
if (state.count === 0) {
if (state.stylesheets) {
insertSuspendedStylesheets(state, state.stylesheets);
}
if (state.unsuspend) {
const unsuspend = state.unsuspend;
state.unsuspend = null;
unsuspend();
}
}
}, SUSPENSEY_IMAGE_TIMEOUT + timeoutOffset);

state.unsuspend = commit;

return () => {
state.unsuspend = null;
clearTimeout(stylesheetTimer);
clearTimeout(imgTimer);
};
};
}
return null;
}

function onUnsuspend(this: SuspendedState) {
this.count--;
if (this.count === 0) {
if (this.stylesheets) {
function checkIfFullyUnsuspended(state: SuspendedState) {
if (state.count === 0 && (state.imgCount === 0 || !state.waitingForImages)) {
if (state.stylesheets) {
// If we haven't actually inserted the stylesheets yet we need to do so now before starting the commit.
// The reason we do this after everything else has finished is because we want to have all the stylesheets
// load synchronously right before mutating. Ideally the new styles will cause a single recalc only on the
// new tree. When we filled up stylesheets we only inlcuded stylesheets with matching media attributes so we
// wait for them to load before actually continuing. We expect this to increase the count above zero
insertSuspendedStylesheets(this, this.stylesheets);
} else if (this.unsuspend) {
const unsuspend = this.unsuspend;
this.unsuspend = null;
insertSuspendedStylesheets(state, state.stylesheets);
} else if (state.unsuspend) {
const unsuspend = state.unsuspend;
state.unsuspend = null;
unsuspend();
}
}
}

function onUnsuspend(this: SuspendedState) {
this.count--;
checkIfFullyUnsuspended(this);
}

function onUnsuspendImg(this: SuspendedState) {
this.imgCount--;
checkIfFullyUnsuspended(this);
}

// We use a value that is type distinct from precedence to track which one is last.
// This ensures there is no collision with user defined precedences. Normally we would
// just track this in module scope but since the precedences are tracked per HoistableRoot
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,7 @@ export function suspendInstance(

export function suspendOnActiveViewTransition(container: Container): void {}

export function waitForCommitToBeReady(): null {
export function waitForCommitToBeReady(timeoutOffset: number): null {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -788,7 +788,7 @@ export function suspendInstance(

export function suspendOnActiveViewTransition(container: Container): void {}

export function waitForCommitToBeReady(): null {
export function waitForCommitToBeReady(timeoutOffset: number): null {
return null;
}

Expand Down
6 changes: 3 additions & 3 deletions packages/react-noop-renderer/src/createReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,9 +361,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
}
}

function waitForCommitToBeReady():
| ((commit: () => mixed) => () => void)
| null {
function waitForCommitToBeReady(
timeoutOffset: number,
): ((commit: () => mixed) => () => void) | null {
const subscription = suspenseyCommitSubscription;
if (subscription !== null) {
suspenseyCommitSubscription = null;
Expand Down
2 changes: 2 additions & 0 deletions packages/react-reconciler/src/ReactFiberTransition.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {createCursor, push, pop} from './ReactFiberStack';
import {
getWorkInProgressRoot,
getWorkInProgressTransitions,
markTransitionStarted,
} from './ReactFiberWorkLoop';
import {
createCache,
Expand Down Expand Up @@ -79,6 +80,7 @@ ReactSharedInternals.S = function onStartTransitionFinishForReconciler(
transition: Transition,
returnValue: mixed,
) {
markTransitionStarted();
if (
typeof returnValue === 'object' &&
returnValue !== null &&
Expand Down
19 changes: 18 additions & 1 deletion packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,11 @@ let didIncludeCommitPhaseUpdate: boolean = false;
// content as it streams in, to minimize jank.
// TODO: Think of a better name for this variable?
let globalMostRecentFallbackTime: number = 0;
// Track the most recent time we started a new Transition. This lets us apply
// heuristics like the suspensey image timeout based on how long we've waited
// already.
let globalMostRecentTransitionTime: number = 0;

const FALLBACK_THROTTLE_MS: number = 300;

// The absolute time for when we should start giving up on rendering
Expand Down Expand Up @@ -1500,10 +1505,18 @@ function commitRootWhenReady(
suspendOnActiveViewTransition(root.containerInfo);
}
}
// For timeouts we use the previous fallback commit for retries and
// the start time of the transition for transitions. This offset
// represents the time already passed.
const timeoutOffset = includesOnlyRetries(lanes)
? globalMostRecentFallbackTime - now()
: includesOnlyTransitions(lanes)
? globalMostRecentTransitionTime - now()
: 0;
// At the end, ask the renderer if it's ready to commit, or if we should
// suspend. If it's not ready, it will return a callback to subscribe to
// a ready event.
const schedulePendingCommit = waitForCommitToBeReady();
const schedulePendingCommit = waitForCommitToBeReady(timeoutOffset);
if (schedulePendingCommit !== null) {
// NOTE: waitForCommitToBeReady returns a subscribe function so that we
// only allocate a function if the commit isn't ready yet. The other
Expand Down Expand Up @@ -2284,6 +2297,10 @@ export function markRenderDerivedCause(fiber: Fiber): void {
}
}

export function markTransitionStarted() {
globalMostRecentTransitionTime = now();
}

export function markCommitTimeOfFallback() {
globalMostRecentFallbackTime = now();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ describe('ReactFiberHostContext', () => {
startSuspendingCommit() {},
suspendInstance(instance, type, props) {},
suspendOnActiveViewTransition(container) {},
waitForCommitToBeReady() {
waitForCommitToBeReady(timeoutOffset: number) {
return null;
},
supportsMutation: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ export function suspendInstance(

export function suspendOnActiveViewTransition(container: Container): void {}

export function waitForCommitToBeReady(): null {
export function waitForCommitToBeReady(timeoutOffset: number): null {
return null;
}

Expand Down
Loading