mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
[Fiber] Adjust the suspensey image/css timeout based on already elapsed time (#34478)
Currently suspensey images doesn't account for how long we've already been waiting. This means that you can for example wait for 300ms for the throttle + 500ms for the images. If a Transition takes a while to complete you can also wait that time + an additional 500ms for the images. This tracks the start time of a Transition so that we can count the timeout starting from when the user interacted or when the last fallback committed (which is where the 300ms throttle is computed from). Creating a single timeline. This also moves the timeout to a central place which I'll use in a follow up.
This commit is contained in:
parent
e12b0bdc3b
commit
e3f191803c
|
|
@ -615,7 +615,7 @@ export function suspendInstance(instance, type, props) {}
|
|||
|
||||
export function suspendOnActiveViewTransition(container) {}
|
||||
|
||||
export function waitForCommitToBeReady() {
|
||||
export function waitForCommitToBeReady(timeoutOffset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -5922,6 +5926,8 @@ export function startSuspendingCommit(): void {
|
|||
};
|
||||
}
|
||||
|
||||
const SUSPENSEY_STYLESHEET_TIMEOUT = 60000;
|
||||
|
||||
const SUSPENSEY_IMAGE_TIMEOUT = 500;
|
||||
|
||||
export function suspendInstance(
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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.',
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {createCursor, push, pop} from './ReactFiberStack';
|
|||
import {
|
||||
getWorkInProgressRoot,
|
||||
getWorkInProgressTransitions,
|
||||
markTransitionStarted,
|
||||
} from './ReactFiberWorkLoop';
|
||||
import {
|
||||
createCache,
|
||||
|
|
@ -79,6 +80,7 @@ ReactSharedInternals.S = function onStartTransitionFinishForReconciler(
|
|||
transition: Transition,
|
||||
returnValue: mixed,
|
||||
) {
|
||||
markTransitionStarted();
|
||||
if (
|
||||
typeof returnValue === 'object' &&
|
||||
returnValue !== null &&
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -2284,6 +2297,10 @@ export function markRenderDerivedCause(fiber: Fiber): void {
|
|||
}
|
||||
}
|
||||
|
||||
export function markTransitionStarted() {
|
||||
globalMostRecentTransitionTime = now();
|
||||
}
|
||||
|
||||
export function markCommitTimeOfFallback() {
|
||||
globalMostRecentFallbackTime = now();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ describe('ReactFiberHostContext', () => {
|
|||
startSuspendingCommit() {},
|
||||
suspendInstance(instance, type, props) {},
|
||||
suspendOnActiveViewTransition(container) {},
|
||||
waitForCommitToBeReady() {
|
||||
waitForCommitToBeReady(timeoutOffset: number) {
|
||||
return null;
|
||||
},
|
||||
supportsMutation: true,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user