@@ -2010,7 +2010,8 @@ function cancelAllViewTransitionAnimations(scope: Element) {
2010
2010
// an issue when it's a new load and slow, yet long enough that you have a chance to load
2011
2011
// it. Otherwise we wait for no reason. The assumption here is that you likely have
2012
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 ;
2014
2015
2015
2016
function customizeViewTransitionError (
2016
2017
error : Object ,
@@ -2080,6 +2081,13 @@ function forceLayout(ownerDocument: Document) {
2080
2081
return ( ownerDocument . documentElement : any ) . clientHeight ;
2081
2082
}
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
+
2083
2091
export function startViewTransition (
2084
2092
suspendedState : null | SuspendedState ,
2085
2093
rootContainer : Container ,
@@ -2108,6 +2116,7 @@ export function startViewTransition(
2108
2116
// $FlowFixMe[prop-missing]
2109
2117
const previousFontLoadingStatus = ownerDocument . fonts . status ;
2110
2118
mutationCallback ( ) ;
2119
+ const blockingPromises : Array < Promise < any > > = [ ] ;
2111
2120
if ( previousFontLoadingStatus === 'loaded' ) {
2112
2121
// Force layout calculation to trigger font loading.
2113
2122
forceLayout ( ownerDocument ) ;
@@ -2119,19 +2128,51 @@ export function startViewTransition(
2119
2128
// This avoids waiting for potentially unrelated fonts that were already loading before.
2120
2129
// Either in an earlier transition or as part of a sync optimistic state. This doesn't
2121
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 ) ;
2133
2132
}
2134
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
+ }
2135
2176
layoutCallback ( ) ;
2136
2177
if ( pendingNavigation ) {
2137
2178
return pendingNavigation . finished . then (
@@ -5909,8 +5950,9 @@ export function preloadResource(resource: Resource): boolean {
5909
5950
export opaque type SuspendedState = {
5910
5951
stylesheets : null | Map < StylesheetResource , HoistableRoot > ,
5911
5952
count : number , // suspensey css and active view transitions
5912
- imgCount : number , // suspensey images
5953
+ imgCount : number , // suspensey images pending to load
5913
5954
imgBytes : number , // number of bytes we estimate needing to download
5955
+ suspenseyImages : Array < HTMLImageElement > , // instances of suspensey images (whether loaded or not)
5914
5956
waitingForImages : boolean , // false when we're no longer blocking on images
5915
5957
unsuspend : null | ( ( ) => void ) ,
5916
5958
} ;
@@ -5921,6 +5963,7 @@ export function startSuspendingCommit(): SuspendedState {
5921
5963
count : 0 ,
5922
5964
imgCount : 0 ,
5923
5965
imgBytes : 0 ,
5966
+ suspenseyImages : [ ] ,
5924
5967
waitingForImages : true ,
5925
5968
// We use a noop function when we begin suspending because if possible we want the
5926
5969
// waitfor step to finish synchronously. If it doesn't we'll return a function to
@@ -5930,6 +5973,16 @@ export function startSuspendingCommit(): SuspendedState {
5930
5973
} ;
5931
5974
}
5932
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
+
5933
5986
export function suspendInstance (
5934
5987
state : SuspendedState ,
5935
5988
instance : Instance ,
@@ -5941,8 +5994,7 @@ export function suspendInstance(
5941
5994
}
5942
5995
if (
5943
5996
// $FlowFixMe[prop-missing]
5944
- typeof instance . decode === 'function' &&
5945
- typeof setTimeout === 'function'
5997
+ typeof instance . decode === 'function'
5946
5998
) {
5947
5999
// If this browser supports decode() API, we use it to suspend waiting on the image.
5948
6000
// The loading should have already started at this point, so it should be enough to
@@ -5952,13 +6004,8 @@ export function suspendInstance(
5952
6004
// specified in the props. This is best practice to know ahead of time but if it's
5953
6005
// unspecified we'll fallback to a guess of 100x100 pixels.
5954
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 ) ) ;
5962
6009
}
5963
6010
const ping = onUnsuspendImg . bind ( state ) ;
5964
6011
// $FlowFixMe[prop-missing]
0 commit comments