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 suspendOnActiveViewTransition(container) {}
|
||||||
|
|
||||||
export function waitForCommitToBeReady() {
|
export function waitForCommitToBeReady(timeoutOffset) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5905,7 +5905,9 @@ export function preloadResource(resource: Resource): boolean {
|
||||||
|
|
||||||
type SuspendedState = {
|
type SuspendedState = {
|
||||||
stylesheets: null | Map<StylesheetResource, HoistableRoot>,
|
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),
|
unsuspend: null | (() => void),
|
||||||
};
|
};
|
||||||
let suspendedState: null | SuspendedState = null;
|
let suspendedState: null | SuspendedState = null;
|
||||||
|
|
@ -5914,6 +5916,8 @@ export function startSuspendingCommit(): void {
|
||||||
suspendedState = {
|
suspendedState = {
|
||||||
stylesheets: null,
|
stylesheets: null,
|
||||||
count: 0,
|
count: 0,
|
||||||
|
imgCount: 0,
|
||||||
|
waitingForImages: true,
|
||||||
// We use a noop function when we begin suspending because if possible we want the
|
// 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
|
// 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
|
// 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;
|
const SUSPENSEY_IMAGE_TIMEOUT = 500;
|
||||||
|
|
||||||
export function suspendInstance(
|
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.
|
// 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
|
// 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.
|
// just call decode() which should also wait for the data to finish loading.
|
||||||
state.count++;
|
state.imgCount++;
|
||||||
const ping = onUnsuspend.bind(state);
|
const ping = onUnsuspendImg.bind(state);
|
||||||
Promise.race([
|
// $FlowFixMe[prop-missing]
|
||||||
// $FlowFixMe[prop-missing]
|
instance.decode().then(ping, ping);
|
||||||
instance.decode(),
|
|
||||||
new Promise(resolve => setTimeout(resolve, SUSPENSEY_IMAGE_TIMEOUT)),
|
|
||||||
]).then(ping, ping);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6067,7 +6070,9 @@ export function suspendOnActiveViewTransition(rootContainer: Container): void {
|
||||||
activeViewTransition.finished.then(ping, ping);
|
activeViewTransition.finished.then(ping, ping);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function waitForCommitToBeReady(): null | ((() => void) => () => void) {
|
export function waitForCommitToBeReady(
|
||||||
|
timeoutOffset: number,
|
||||||
|
): null | ((() => void) => () => void) {
|
||||||
if (suspendedState === null) {
|
if (suspendedState === null) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug.',
|
'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
|
// We need to check the count again because the inserted stylesheets may have led to new
|
||||||
// tasks to wait on.
|
// tasks to wait on.
|
||||||
if (state.count > 0) {
|
if (state.count > 0 || state.imgCount > 0) {
|
||||||
return commit => {
|
return commit => {
|
||||||
// We almost never want to show content before its styles have loaded. But
|
// 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
|
// 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;
|
state.unsuspend = null;
|
||||||
unsuspend();
|
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;
|
state.unsuspend = commit;
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
state.unsuspend = null;
|
state.unsuspend = null;
|
||||||
clearTimeout(stylesheetTimer);
|
clearTimeout(stylesheetTimer);
|
||||||
|
clearTimeout(imgTimer);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUnsuspend(this: SuspendedState) {
|
function checkIfFullyUnsuspended(state: SuspendedState) {
|
||||||
this.count--;
|
if (state.count === 0 && (state.imgCount === 0 || !state.waitingForImages)) {
|
||||||
if (this.count === 0) {
|
if (state.stylesheets) {
|
||||||
if (this.stylesheets) {
|
|
||||||
// If we haven't actually inserted the stylesheets yet we need to do so now before starting the commit.
|
// 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
|
// 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
|
// 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
|
// 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
|
// wait for them to load before actually continuing. We expect this to increase the count above zero
|
||||||
insertSuspendedStylesheets(this, this.stylesheets);
|
insertSuspendedStylesheets(state, state.stylesheets);
|
||||||
} else if (this.unsuspend) {
|
} else if (state.unsuspend) {
|
||||||
const unsuspend = this.unsuspend;
|
const unsuspend = state.unsuspend;
|
||||||
this.unsuspend = null;
|
state.unsuspend = null;
|
||||||
unsuspend();
|
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.
|
// 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
|
// 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
|
// 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 suspendOnActiveViewTransition(container: Container): void {}
|
||||||
|
|
||||||
export function waitForCommitToBeReady(): null {
|
export function waitForCommitToBeReady(timeoutOffset: number): null {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -788,7 +788,7 @@ export function suspendInstance(
|
||||||
|
|
||||||
export function suspendOnActiveViewTransition(container: Container): void {}
|
export function suspendOnActiveViewTransition(container: Container): void {}
|
||||||
|
|
||||||
export function waitForCommitToBeReady(): null {
|
export function waitForCommitToBeReady(timeoutOffset: number): null {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -361,9 +361,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function waitForCommitToBeReady():
|
function waitForCommitToBeReady(
|
||||||
| ((commit: () => mixed) => () => void)
|
timeoutOffset: number,
|
||||||
| null {
|
): ((commit: () => mixed) => () => void) | null {
|
||||||
const subscription = suspenseyCommitSubscription;
|
const subscription = suspenseyCommitSubscription;
|
||||||
if (subscription !== null) {
|
if (subscription !== null) {
|
||||||
suspenseyCommitSubscription = null;
|
suspenseyCommitSubscription = null;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import {createCursor, push, pop} from './ReactFiberStack';
|
||||||
import {
|
import {
|
||||||
getWorkInProgressRoot,
|
getWorkInProgressRoot,
|
||||||
getWorkInProgressTransitions,
|
getWorkInProgressTransitions,
|
||||||
|
markTransitionStarted,
|
||||||
} from './ReactFiberWorkLoop';
|
} from './ReactFiberWorkLoop';
|
||||||
import {
|
import {
|
||||||
createCache,
|
createCache,
|
||||||
|
|
@ -79,6 +80,7 @@ ReactSharedInternals.S = function onStartTransitionFinishForReconciler(
|
||||||
transition: Transition,
|
transition: Transition,
|
||||||
returnValue: mixed,
|
returnValue: mixed,
|
||||||
) {
|
) {
|
||||||
|
markTransitionStarted();
|
||||||
if (
|
if (
|
||||||
typeof returnValue === 'object' &&
|
typeof returnValue === 'object' &&
|
||||||
returnValue !== null &&
|
returnValue !== null &&
|
||||||
|
|
|
||||||
|
|
@ -475,6 +475,11 @@ let didIncludeCommitPhaseUpdate: boolean = false;
|
||||||
// content as it streams in, to minimize jank.
|
// content as it streams in, to minimize jank.
|
||||||
// TODO: Think of a better name for this variable?
|
// TODO: Think of a better name for this variable?
|
||||||
let globalMostRecentFallbackTime: number = 0;
|
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;
|
const FALLBACK_THROTTLE_MS: number = 300;
|
||||||
|
|
||||||
// The absolute time for when we should start giving up on rendering
|
// The absolute time for when we should start giving up on rendering
|
||||||
|
|
@ -1500,10 +1505,18 @@ function commitRootWhenReady(
|
||||||
suspendOnActiveViewTransition(root.containerInfo);
|
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
|
// 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
|
// suspend. If it's not ready, it will return a callback to subscribe to
|
||||||
// a ready event.
|
// a ready event.
|
||||||
const schedulePendingCommit = waitForCommitToBeReady();
|
const schedulePendingCommit = waitForCommitToBeReady(timeoutOffset);
|
||||||
if (schedulePendingCommit !== null) {
|
if (schedulePendingCommit !== null) {
|
||||||
// NOTE: waitForCommitToBeReady returns a subscribe function so that we
|
// NOTE: waitForCommitToBeReady returns a subscribe function so that we
|
||||||
// only allocate a function if the commit isn't ready yet. The other
|
// 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() {
|
export function markCommitTimeOfFallback() {
|
||||||
globalMostRecentFallbackTime = now();
|
globalMostRecentFallbackTime = now();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ describe('ReactFiberHostContext', () => {
|
||||||
startSuspendingCommit() {},
|
startSuspendingCommit() {},
|
||||||
suspendInstance(instance, type, props) {},
|
suspendInstance(instance, type, props) {},
|
||||||
suspendOnActiveViewTransition(container) {},
|
suspendOnActiveViewTransition(container) {},
|
||||||
waitForCommitToBeReady() {
|
waitForCommitToBeReady(timeoutOffset: number) {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
supportsMutation: true,
|
supportsMutation: true,
|
||||||
|
|
|
||||||
|
|
@ -571,7 +571,7 @@ export function suspendInstance(
|
||||||
|
|
||||||
export function suspendOnActiveViewTransition(container: Container): void {}
|
export function suspendOnActiveViewTransition(container: Container): void {}
|
||||||
|
|
||||||
export function waitForCommitToBeReady(): null {
|
export function waitForCommitToBeReady(timeoutOffset: number): null {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user