2010
// an issue when it's a new load and slow, yet long enough that you have a chance to load
2011
// it. Otherwise we wait for no reason. The assumption here is that you likely have
2012
// either cached the font or preloaded it earlier.
2013
-const SUSPENSEY_FONT_TIMEOUT = 500;
2013
+// This timeout is also used for Suspensey Images when they're blocking a View Transition.
2014
+const SUSPENSEY_FONT_AND_IMAGE_TIMEOUT = 500;
2015
2016
function customizeViewTransitionError(
2017
error: Object,
2081
return (ownerDocument.documentElement: any).clientHeight;
2082
}
2083
2084
+function waitForImageToLoad(this: HTMLImageElement, resolve: () => void) {
2085
+ // TODO: Use decode() instead of the load event here once the fix in
2086
+ // https://issues.chromium.org/issues/420748301 has propagated fully.
2087
+ this.addEventListener('load', resolve);
2088
+ this.addEventListener('error', resolve);
2089
+}
2090
+
2091
export function startViewTransition(
2092
suspendedState: null | SuspendedState,
2093
rootContainer: Container,
2116
// $FlowFixMe[prop-missing]
2117
const previousFontLoadingStatus = ownerDocument.fonts.status;
2118
mutationCallback();
2119
+ const blockingPromises: Array<Promise<any>> = [];
2120
if (previousFontLoadingStatus === 'loaded') {
2121
// Force layout calculation to trigger font loading.
2122
forceLayout(ownerDocument);
2128
// This avoids waiting for potentially unrelated fonts that were already loading before.
2129
// Either in an earlier transition or as part of a sync optimistic state. This doesn't
2130
// include preloads that happened earlier.
2122
- const fontsReady = Promise.race([
2123
- // $FlowFixMe[prop-missing]
2124
- ownerDocument.fonts.ready,
2125
- new Promise(resolve =>
2126
- setTimeout(resolve, SUSPENSEY_FONT_TIMEOUT),
2127
- ),
2128
- ]).then(layoutCallback, layoutCallback);
2129
- const allReady = pendingNavigation
2130
- ? Promise.allSettled([pendingNavigation.finished, fontsReady])
2131
- : fontsReady;
2132
- return allReady.then(afterMutationCallback, afterMutationCallback);
2131
+ blockingPromises.push(ownerDocument.fonts.ready);
2132
}
2133
}
2134
+ if (suspendedState !== null) {
2135
+ // Suspend on any images that still haven't loaded and are in the viewport.
2136
+ const suspenseyImages = suspendedState.suspenseyImages;
2137
+ const blockingIndexSnapshot = blockingPromises.length;
2138
+ let imgBytes = 0;
2139
+ for (let i = 0; i < suspenseyImages.length; i++) {
2140
+ const suspenseyImage = suspenseyImages[i];
2141
+ if (!suspenseyImage.complete) {
2142
+ const rect = suspenseyImage.getBoundingClientRect();
2143
+ const inViewport =
2144
+ rect.bottom > 0 &&
2145
+ rect.right > 0 &&
2146
+ rect.top < ownerWindow.innerHeight &&
2147
+ rect.left < ownerWindow.innerWidth;
2148
+ if (inViewport) {
2149
+ imgBytes += estimateImageBytes(suspenseyImage);
2150
+ if (imgBytes > estimatedBytesWithinLimit) {
2151
+ // We don't think we'll be able to download all the images within
2152
+ // the timeout. Give up. Rewind to only block on fonts, if any.
2153
+ blockingPromises.length = blockingIndexSnapshot;
2154
+ break;
2155
+ }
2156
+ const loadingImage = new Promise(
2157
+ waitForImageToLoad.bind(suspenseyImage),
2158
+ );
2159
+ blockingPromises.push(loadingImage);
2160
+ }
2161
+ }
2162
+ }
2163
+ }
2164
+ if (blockingPromises.length > 0) {
2165
+ const blockingReady = Promise.race([
2166
+ Promise.all(blockingPromises),
2167
+ new Promise(resolve =>
2168
+ setTimeout(resolve, SUSPENSEY_FONT_AND_IMAGE_TIMEOUT),
2169
+ ),
2170
+ ]).then(layoutCallback, layoutCallback);
2171
+ const allReady = pendingNavigation
2172
+ ? Promise.allSettled([pendingNavigation.finished, blockingReady])
2173
+ : blockingReady;
2174
+ return allReady.then(afterMutationCallback, afterMutationCallback);
2175
+ }
2176
layoutCallback();
2177
if (pendingNavigation) {
2178
return pendingNavigation.finished.then(
5950
export opaque type SuspendedState = {
5951
stylesheets: null | Map<StylesheetResource, HoistableRoot>,
5952
count: number, // suspensey css and active view transitions
5912
- imgCount: number, // suspensey images
5953
+ imgCount: number, // suspensey images pending to load
5954
imgBytes: number, // number of bytes we estimate needing to download
5955
+ suspenseyImages: Array<HTMLImageElement>, // instances of suspensey images (whether loaded or not)
5956
waitingForImages: boolean, // false when we're no longer blocking on images
5957
unsuspend: null | (() => void),
5958
};
5963
count: 0,
5964
imgCount: 0,
5965
imgBytes: 0,
5966
+ suspenseyImages: [],
5967
waitingForImages: true,
5968
// We use a noop function when we begin suspending because if possible we want the
5969
// waitfor step to finish synchronously. If it doesn't we'll return a function to
5973
};
5974
}
5975
5976
+function estimateImageBytes(instance: HTMLImageElement): number {
5977
+ const width: number = instance.width || 100;
5978
+ const height: number = instance.height || 100;
5979
+ const pixelRatio: number =
5980
+ typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;
5981
+ const pixelsToDownload = width * height * pixelRatio;
5982
+ const AVERAGE_BYTE_PER_PIXEL = 0.25;
5983
+ return pixelsToDownload * AVERAGE_BYTE_PER_PIXEL;
5984
+}
5985
+
5986
export function suspendInstance(
5987
state: SuspendedState,
5988
instance: Instance,
5994
}
5995
if (
5996
// $FlowFixMe[prop-missing]
5944
- typeof instance.decode === 'function' &&
5945
- typeof setTimeout === 'function'
5997
+ typeof instance.decode === 'function'
5998
) {
5999
// If this browser supports decode() API, we use it to suspend waiting on the image.
6000
// The loading should have already started at this point, so it should be enough to
6004
// specified in the props. This is best practice to know ahead of time but if it's
6005
// unspecified we'll fallback to a guess of 100x100 pixels.
6006
if (!(instance: any).complete) {
5955
- const width: number = (instance: any).width || 100;
5956
- const height: number = (instance: any).height || 100;
5957
- const pixelRatio: number =
5958
- typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;
5959
- const pixelsToDownload = width * height * pixelRatio;
5960
- const AVERAGE_BYTE_PER_PIXEL = 0.25;
5961
- state.imgBytes += pixelsToDownload * AVERAGE_BYTE_PER_PIXEL;
6007
+ state.imgBytes += estimateImageBytes((instance: any));
6008
+ state.suspenseyImages.push((instance: any));
6009
}
6010
const ping = onUnsuspendImg.bind(state);
6011
// $FlowFixMe[prop-missing]